@event4u/agent-config 9.16.0 → 9.18.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/AGENTS.md +1 -1
- package/CHANGELOG.md +80 -130
- package/README.md +1 -1
- package/dist/agent-src/commands/agent-handoff.md +100 -48
- package/dist/agent-src/commands/challenge-me/vision.md +5 -0
- package/dist/agent-src/commands/commit.md +1 -1
- package/dist/agent-src/commands/condense.md +19 -64
- package/dist/agent-src/commands/evals/agent-handoff.json +13 -1
- package/dist/agent-src/commands/evals/rule-compliance-audit.json +46 -0
- package/dist/agent-src/commands/feature/plan.md +20 -0
- package/dist/agent-src/commands/feature/roadmap.md +19 -0
- package/dist/agent-src/commands/memory/promote.md +14 -1
- package/dist/agent-src/commands/mode.md +9 -5
- package/dist/agent-src/commands/optimize/deep.md +72 -11
- package/dist/agent-src/commands/pr/create.md +29 -0
- package/dist/agent-src/commands/roadmap/create.md +29 -0
- package/dist/agent-src/commands/rule-compliance-audit.md +28 -5
- package/dist/agent-src/contexts/authority/commit-mechanics.md +5 -5
- package/dist/agent-src/contexts/communication/rules-auto/guidelines-mechanics.md +1 -0
- package/dist/agent-src/contexts/communication/rules-auto/skill-quality-mechanics.md +1 -1
- package/dist/agent-src/contexts/communication/rules-auto/think-before-action-mechanics.md +6 -1
- package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +4 -1
- package/dist/agent-src/contexts/execution/autonomy-mechanics.md +27 -1
- package/dist/agent-src/contexts/execution/plan-confidence-gate.md +133 -0
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +5 -2
- package/dist/agent-src/profiles/agency.yml +3 -3
- package/dist/agent-src/profiles/content_creator.yml +2 -2
- package/dist/agent-src/profiles/developer.yml +2 -2
- package/dist/agent-src/profiles/finance.yml +1 -1
- package/dist/agent-src/profiles/founder.yml +2 -2
- package/dist/agent-src/profiles/ops.yml +2 -2
- package/dist/agent-src/rules/active-remediation.md +2 -0
- package/dist/agent-src/rules/architecture.md +4 -2
- package/dist/agent-src/rules/augment-edit-discipline.md +2 -0
- package/dist/agent-src/rules/autonomous-execution.md +1 -1
- package/dist/agent-src/rules/brand-consistency.md +10 -62
- package/dist/agent-src/rules/brand-source-of-truth.md +62 -11
- package/dist/agent-src/rules/broken-access-control.md +4 -0
- package/dist/agent-src/rules/cli-output-handling.md +5 -0
- package/dist/agent-src/rules/code-comment-discipline.md +3 -0
- package/dist/agent-src/rules/commit-conventions.md +2 -0
- package/dist/agent-src/rules/context-hygiene.md +3 -2
- package/dist/agent-src/rules/cross-source-consistency.md +3 -0
- package/dist/agent-src/rules/design-fidelity.md +2 -0
- package/dist/agent-src/rules/doc-screenshot-hygiene.md +2 -0
- package/dist/agent-src/rules/docker-commands.md +4 -0
- package/dist/agent-src/rules/domain-adoption-policy.md +2 -0
- package/dist/agent-src/rules/domain-safety-disclaimer.md +5 -2
- package/dist/agent-src/rules/domain-safety-pii.md +1 -1
- package/dist/agent-src/rules/engineering-safety-floor.md +2 -0
- package/dist/agent-src/rules/framework-neutrality-in-generic-skills.md +9 -0
- package/dist/agent-src/rules/image-likeness-and-rights.md +4 -0
- package/dist/agent-src/rules/improve-before-implement.md +4 -0
- package/dist/agent-src/rules/laravel-routing.md +4 -0
- package/dist/agent-src/rules/legal-safety-floor.md +3 -0
- package/dist/agent-src/rules/lethal-trifecta-guard.md +3 -0
- package/dist/agent-src/rules/media-governance-routing.md +5 -0
- package/dist/agent-src/rules/minimal-safe-diff.md +2 -0
- package/dist/agent-src/rules/no-roadmap-references.md +2 -0
- package/dist/agent-src/rules/notes-first-reasoning.md +2 -0
- package/dist/agent-src/rules/output-discipline.md +9 -2
- package/dist/agent-src/rules/php-coding.md +2 -0
- package/dist/agent-src/rules/prefer-enums-over-literals.md +3 -0
- package/dist/agent-src/rules/provider-lifecycle-discipline.md +5 -0
- package/dist/agent-src/rules/roadmap-ci-steps-policy.md +2 -0
- package/dist/agent-src/rules/roadmap-progress-sync.md +2 -0
- package/dist/agent-src/rules/rule-type-governance.md +2 -0
- package/dist/agent-src/rules/scale-discipline.md +3 -0
- package/dist/agent-src/rules/secret-vcs-guard.md +2 -0
- package/dist/agent-src/rules/security-sensitive-stop.md +9 -2
- package/dist/agent-src/rules/senior-engineering-discipline.md +6 -0
- package/dist/agent-src/rules/skill-quality.md +2 -0
- package/dist/agent-src/rules/source-confidentiality.md +4 -0
- package/dist/agent-src/rules/source-discovery-gate.md +3 -0
- package/dist/agent-src/rules/source-of-truth.md +2 -0
- package/dist/agent-src/rules/strategy-safety-floor.md +2 -0
- package/dist/agent-src/rules/think-before-action.md +5 -0
- package/dist/agent-src/rules/token-budget-discipline.md +2 -2
- package/dist/agent-src/rules/ui-audit-gate.md +2 -0
- package/dist/agent-src/rules/user-interaction.md +3 -4
- package/dist/agent-src/skills/build-buy-partner/SKILL.md +1 -1
- package/dist/agent-src/skills/copilot-agents-optimization/SKILL.md +4 -4
- package/dist/agent-src/skills/copilot-config/SKILL.md +1 -1
- package/dist/agent-src/skills/deep-reading-analyst/SKILL.md +7 -4
- package/dist/agent-src/skills/description-assist/SKILL.md +1 -1
- package/dist/agent-src/skills/design-intelligence/SKILL.md +6 -180
- package/dist/agent-src/skills/design-intelligence/references/context-and-registers.md +100 -0
- package/dist/agent-src/skills/design-intelligence/references/integration-mapping.md +91 -0
- package/dist/agent-src/skills/design-review/SKILL.md +4 -141
- package/dist/agent-src/skills/design-review/references/review-communication.md +92 -0
- package/dist/agent-src/skills/design-review/references/verification-automation.md +61 -0
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +8 -27
- package/dist/agent-src/skills/existing-ui-audit/references/anti-slop-cross-reference.md +31 -0
- package/dist/agent-src/skills/fe-design/SKILL.md +4 -289
- package/dist/agent-src/skills/fe-design/references/design-patterns.md +227 -0
- package/dist/agent-src/skills/fe-design/references/design-read-and-memory.md +75 -0
- package/dist/agent-src/skills/market-entry-analysis/SKILL.md +1 -1
- package/dist/agent-src/skills/persona-writing/SKILL.md +2 -2
- package/dist/agent-src/skills/readme-reviewer/SKILL.md +2 -1
- package/dist/agent-src/skills/roadmap-management/SKILL.md +10 -0
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +12 -1
- package/dist/agent-src/skills/rule-writing/SKILL.md +4 -2
- package/dist/agent-src/skills/skill-reviewer/SKILL.md +1 -1
- package/dist/agent-src/skills/skill-writing/SKILL.md +2 -2
- package/dist/agent-src/skills/tailwind-engineer/SKILL.md +3 -1
- package/dist/agent-src/skills/token-optimizer/SKILL.md +2 -2
- package/dist/agent-src/skills/ui-component-architect/SKILL.md +6 -3
- package/dist/agent-src/templates/agent-settings.md +13 -15
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +6 -2
- package/dist/agent-src/templates/persona.md +2 -2
- package/dist/agent-src/templates/roadmaps.md +13 -1
- package/dist/agent-src/templates/rule.md +1 -1
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/_fix_lane.ts +52 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +8 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/design.ts +6 -0
- package/dist/agent-src/templates/scripts/work_engine/intent/classify.ts +56 -4
- package/dist/agent-src/templates/skill.md +1 -1
- package/dist/cli/registry.js +3 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/chunk-AIIUU73N.js +2040 -0
- package/dist/cli-delegate/{chunk-ZXXJLL5K.js → chunk-BSLUHV2F.js} +127 -1487
- package/dist/cli-delegate/{chunk-H574KOUH.js → chunk-ELMFOQPW.js} +35 -1
- package/dist/cli-delegate/chunk-LF2KKJ4Q.js +183 -0
- package/dist/cli-delegate/chunk-VSYT7OHO.js +1382 -0
- package/dist/cli-delegate/chunk-XGA2NTU4.js +3727 -0
- package/dist/cli-delegate/cmd_conformance.js +4 -3
- package/dist/cli-delegate/cmd_converge.js +3 -3
- package/dist/cli-delegate/cmd_doctor.js +3 -2
- package/dist/cli-delegate/cmd_explain.js +36 -36
- package/dist/cli-delegate/cmd_export.js +4 -4
- package/dist/cli-delegate/cmd_handoff.js +1169 -0
- package/dist/cli-delegate/cmd_memory_get.js +35 -3754
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_prune.js +4 -4
- package/dist/cli-delegate/cmd_route_audit.js +249 -0
- package/dist/cli-delegate/cmd_route_explain.js +285 -0
- package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
- package/dist/cli-delegate/cmd_sync.js +4 -4
- package/dist/cli-delegate/cmd_uninstall.js +4 -4
- package/dist/cli-delegate/cmd_update.js +3 -3
- package/dist/cli-delegate/cmd_upgrade.js +3 -3
- package/dist/cli-delegate/cmd_validate.js +4 -4
- package/dist/cli-delegate/cmd_versions.js +3 -3
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +84 -84
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/dispatch.js +602 -507
- package/dist/install/install.mjs +34 -0
- package/dist/mcp/awesome-mcp-servers.row.md +1 -1
- package/dist/mcp/mcp-cloudflare-catalogue.json +2 -2
- package/dist/mcp/registry-manifest.json +4 -4
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +25 -3
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/agent_src.js +39 -6
- package/dist/scripts/_lib/agent_src.js.map +1 -1
- package/dist/server/schemas/settings.js +25 -0
- package/dist/server/schemas/settings.js.map +1 -1
- package/docs/guidelines/abstraction-thresholds.md +53 -0
- package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +1 -1
- package/docs/guidelines/agent-infra/asking-and-brevity-examples.md +7 -0
- package/docs/guidelines/agent-infra/context-hygiene-mechanics.md +2 -2
- package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +5 -3
- package/docs/guidelines/agent-infra/size-and-scope.md +8 -2
- package/docs/guidelines/component-oriented-and-oop-development.md +11 -6
- package/docs/guidelines/docs/readme-size-and-splitting.md +5 -2
- package/llms.txt +1 -0
- package/package.json +1 -1
- package/src/agent-src/templates/scripts/work_engine/directives/ui/_fix_lane.ts +52 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +8 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/design.ts +6 -0
- package/src/agent-src/templates/scripts/work_engine/intent/classify.ts +56 -4
- package/src/config/agent-settings.template.yml +68 -0
- package/src/config/evaluator-budgets.json +8 -11
- package/src/config/gate-coverage.yml +70 -0
- package/src/scripts/_cli/cmd_explain.ts +41 -36
- package/src/scripts/_cli/cmd_handoff.ts +405 -0
- package/src/scripts/_cli/cmd_route_audit.ts +290 -0
- package/src/scripts/_cli/cmd_route_explain.ts +269 -0
- package/src/scripts/_cli/handoff_generate.ts +453 -0
- package/src/scripts/_cli/handoff_sessions.ts +435 -0
- package/src/scripts/_dispatch.bash +32 -0
- package/src/scripts/_lib/agent_src.ts +36 -6
- package/src/scripts/_lib/git_env.ts +40 -0
- package/src/scripts/_lib/md_table.ts +27 -0
- package/src/scripts/_lib/planning_settings.ts +43 -0
- package/src/scripts/_lib/release_material.ts +156 -0
- package/src/scripts/_lib/router_match.ts +301 -0
- package/src/scripts/annotate_r1_outcomes.ts +258 -0
- package/src/scripts/check_completion_review.ts +1140 -0
- package/src/scripts/check_condensation.ts +2 -1
- package/src/scripts/check_finding_dispositions.ts +278 -0
- package/src/scripts/check_pack_size.ts +4 -3
- package/src/scripts/check_references.ts +1 -0
- package/src/scripts/check_release_highlights.ts +300 -0
- package/src/scripts/check_release_pr_shape.ts +4 -0
- package/src/scripts/check_release_surface_equality.ts +132 -0
- package/src/scripts/check_reply_consistency.ts +187 -138
- package/src/scripts/compile_router.ts +20 -3
- package/src/scripts/dispatch_r2_reviewer.ts +968 -0
- package/src/scripts/eval_ui_triviality.ts +156 -0
- package/src/scripts/handoff_context_hook.ts +148 -0
- package/src/scripts/hook_manifest.yaml +17 -7
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/lint_abstraction_thresholds.ts +327 -0
- package/src/scripts/lint_framework_leakage_allowlist.json +2 -2
- package/src/scripts/lint_plan_risk_register.ts +931 -0
- package/src/scripts/lint_profile_personas.ts +153 -0
- package/src/scripts/lint_token_budget_discipline.ts +148 -0
- package/src/scripts/lint_trigger_collisions.ts +257 -0
- package/src/scripts/release.ts +156 -32
- package/src/scripts/release_drill.ts +491 -0
- package/src/scripts/router_telemetry.ts +16 -290
- package/src/scripts/schemas/command.schema.json +33 -0
- package/src/scripts/schemas/rule.schema.json +10 -0
- package/src/scripts/self_review_gate.ts +41 -3
- package/src/scripts/skill_linter.ts +3 -1
- package/src/scripts/smoke/router.sh +1 -1
- package/src/scripts/update_counts.ts +60 -12
- package/src/server/schemas/settings.ts +41 -0
- package/dist/cli-delegate/{chunk-XX6TOPD7.js → chunk-WR6GTRHJ.js} +3 -3
|
@@ -0,0 +1,968 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Gate R2 reviewer dispatcher — deterministic assembler for the fresh-subagent
|
|
4
|
+
* completion review (docs/contracts/plan-review-gates.md §5, verdict #18).
|
|
5
|
+
*
|
|
6
|
+
* The Phase-1 reviewer is a FRESH subagent without the implementation
|
|
7
|
+
* context; its input is never assembled by the implementing agent. This
|
|
8
|
+
* script IS that deterministic dispatcher: it computes the branch diff,
|
|
9
|
+
* extracts the roadmap's Acceptance Criteria block, hashes every input,
|
|
10
|
+
* writes the reviewer input package (`<out-dir>/<slug>.review-input/`) and
|
|
11
|
+
* the findings-artifact skeleton (`<out-dir>/<slug>.findings.md`) carrying
|
|
12
|
+
* the verifiable context manifest. It calls NO LLM itself — the host agent
|
|
13
|
+
* dispatches a fresh subagent (/judge:on-diff machinery) at the package.
|
|
14
|
+
*
|
|
15
|
+
* Modes:
|
|
16
|
+
* dispatch (default) — assemble package + skeleton.
|
|
17
|
+
* --verify <findings-file> — re-derive the manifest hashes from the
|
|
18
|
+
* CURRENT repo state and compare against the recorded inputs.
|
|
19
|
+
* --verify-current — compute the current review scope, SELECT the artefacts
|
|
20
|
+
* relevant to it (contract §2.6), and `--verify` each. The selection lives
|
|
21
|
+
* here rather than in a caller's shell loop on purpose: `agents/evidence/
|
|
22
|
+
* reviews/` is tracked and accumulates (§2.6), so "verify every
|
|
23
|
+
* `*.findings.md`" mismatches every foreign artefact by construction and
|
|
24
|
+
* would red the next gated PR — directory-wide poisoning. Selecting by
|
|
25
|
+
* grepping the header for `HEAD` is equally wrong (§2.0 proves it matches
|
|
26
|
+
* nothing), so the only correct selector re-derives the scope hash, which
|
|
27
|
+
* is exactly what this mode does.
|
|
28
|
+
*
|
|
29
|
+
* This is a dispatcher, not a gate — it emits no `scanned:` line and is not
|
|
30
|
+
* registered in gate-coverage.yml.
|
|
31
|
+
*
|
|
32
|
+
* It also OWNS the review-scope hash (contract §2.1): the single definition of
|
|
33
|
+
* what a completion review is bound to. `check_completion_review` imports it
|
|
34
|
+
* rather than restating it — a divergence between the two would silently
|
|
35
|
+
* re-break the gate.
|
|
36
|
+
*
|
|
37
|
+
* Exit codes: 0 = ok / manifest verified, 1 = policy violation (empty diff,
|
|
38
|
+
* refuse-overwrite, missing manifest, manifest mismatch), 2 = internal error
|
|
39
|
+
* (bad ref, unreadable file, crash).
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
import { execFileSync } from 'node:child_process';
|
|
43
|
+
import { createHash } from 'node:crypto';
|
|
44
|
+
import * as fs from 'node:fs';
|
|
45
|
+
import * as path from 'node:path';
|
|
46
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
47
|
+
|
|
48
|
+
// `--verify-current` reuses the validator's §2.6 relevance notion instead of
|
|
49
|
+
// restating it (a restated copy is what re-breaks these gates — see the
|
|
50
|
+
// single-definition rule for the scope hash below). The resulting import cycle
|
|
51
|
+
// (validator → dispatcher for the scope hash, dispatcher → validator for
|
|
52
|
+
// relevance) is safe: neither module calls the other at module-evaluation
|
|
53
|
+
// time, and each CLI entry guard only fires for its own argv[1].
|
|
54
|
+
import { gitEnv } from './_lib/git_env.js';
|
|
55
|
+
import { completionReviewDisabled } from './_lib/planning_settings.js';
|
|
56
|
+
import { artifactRelevance } from './check_completion_review.js';
|
|
57
|
+
|
|
58
|
+
export interface ManifestInputs {
|
|
59
|
+
diffSha: string; // provenance only — never compared
|
|
60
|
+
scopeHash: string;
|
|
61
|
+
roadmap: string; // path, or 'none'
|
|
62
|
+
roadmapHash: string; // sha256, or 'none'
|
|
63
|
+
acHash: string; // sha256, or 'none'
|
|
64
|
+
dispatched: string; // ISO YYYY-MM-DDTHH:MM:SSZ
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface ExpectedHashes {
|
|
68
|
+
scope_hash: string;
|
|
69
|
+
roadmap_hash: string;
|
|
70
|
+
ac_hash: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface ParsedManifest {
|
|
74
|
+
diff_sha: string;
|
|
75
|
+
scope_hash: string;
|
|
76
|
+
roadmap: string;
|
|
77
|
+
roadmap_hash: string;
|
|
78
|
+
ac_hash: string;
|
|
79
|
+
dispatched: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function sha256(text: string): string {
|
|
83
|
+
return createHash('sha256').update(text, 'utf8').digest('hex');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// Review scope — the single source of the R2 binding (contract §2.1)
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The review artefacts are excluded from the reviewed scope on purpose.
|
|
92
|
+
*
|
|
93
|
+
* A completion review is bound to the CONTENT it reviewed, never to a commit:
|
|
94
|
+
* §2.5 requires the findings artifact to be committed, and CI only ever sees
|
|
95
|
+
* committed state — so a head-sha binding is unsatisfiable by construction
|
|
96
|
+
* (committing the artifact moves HEAD past the recorded sha, and on
|
|
97
|
+
* `pull_request` the checkout is a synthetic merge commit whose sha no
|
|
98
|
+
* dispatcher could have recorded). Excluding `agents/evidence/reviews` means
|
|
99
|
+
* writing, editing, or committing the findings artifact cannot change the
|
|
100
|
+
* scope hash, and `base...HEAD` yields the same net diff on a branch head and
|
|
101
|
+
* on a merge commit of that branch.
|
|
102
|
+
*
|
|
103
|
+
* `agents/evidence/metrics` is excluded for the SAME reason, not as tidiness:
|
|
104
|
+
* contract §7 mandates appending the R2 outcome event (`r2_review` /
|
|
105
|
+
* `r2_honest_null` / `r2_skip`) to the tracked
|
|
106
|
+
* `agents/evidence/metrics/gate-metrics.jsonl`. Committing that mandated event
|
|
107
|
+
* would otherwise change the scope hash and turn the very artifact that just
|
|
108
|
+
* recorded the review into a `stale-review` block — the self-invalidation class
|
|
109
|
+
* §2.0 exists to eliminate, re-entering through a sibling path. Any future
|
|
110
|
+
* gate-owned evidence path that the gate itself writes belongs in this list.
|
|
111
|
+
*/
|
|
112
|
+
export const REVIEW_SCOPE_EXCLUDES: readonly string[] = [
|
|
113
|
+
':(exclude,top)agents/evidence/reviews',
|
|
114
|
+
':(exclude,top)agents/evidence/metrics',
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Config overrides for a byte-stable diff — the knobs no diff FLAG can pin.
|
|
119
|
+
*
|
|
120
|
+
* `core.quotePath=false` un-quotes non-ASCII pathnames (`"a/w\303\251ird"` →
|
|
121
|
+
* `a/wéird`), changing the bytes of every header line that names such a file.
|
|
122
|
+
* `true` is git's default, so this restores the default rather than choosing a
|
|
123
|
+
* new one.
|
|
124
|
+
*
|
|
125
|
+
* `core.attributesFile=/dev/null` drops the per-USER attributes file. That is a
|
|
126
|
+
* different layer from `--no-textconv`, which only suppresses a `textconv`
|
|
127
|
+
* filter: a `-diff` (or `binary`) attribute replaces the entire patch body with
|
|
128
|
+
* `Binary files a/x and b/x differ`. One developer carrying a global `*.ts -diff`
|
|
129
|
+
* entry would otherwise record a different scope hash for identical content — a
|
|
130
|
+
* cross-machine `manifest mismatch (stale review)` that no content change
|
|
131
|
+
* explains.
|
|
132
|
+
*
|
|
133
|
+
* RESIDUAL, named rather than papered over: this pins the USER layer only.
|
|
134
|
+
* A **tracked** `.gitattributes` is part of the reviewed content and therefore
|
|
135
|
+
* identical on every checkout, so it is not a cross-machine variable at all.
|
|
136
|
+
* `$GIT_DIR/info/attributes` IS one — per-clone, untracked, and with no
|
|
137
|
+
* command-line override — so it stays un-neutralised; likewise the system-wide
|
|
138
|
+
* `etc/gitattributes`, which only the `GIT_ATTR_NOSYSTEM` environment variable
|
|
139
|
+
* disables. Both are accepted residuals, not gaps this list silently covers.
|
|
140
|
+
*
|
|
141
|
+
* `diff.orderFile` reorders the per-file sections of the output; the documented
|
|
142
|
+
* canceller is the `-O/dev/null` flag (see {@link REVIEW_SCOPE_DIFF_FLAGS}),
|
|
143
|
+
* which is why it is not listed here.
|
|
144
|
+
*/
|
|
145
|
+
export const REVIEW_SCOPE_GIT_CONFIG: readonly string[] = [
|
|
146
|
+
'-c',
|
|
147
|
+
'core.quotePath=true',
|
|
148
|
+
'-c',
|
|
149
|
+
'core.attributesFile=/dev/null',
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The pinned patch-output flag set (contract §2.0).
|
|
154
|
+
*
|
|
155
|
+
* The scope hash is THE cross-machine binding: a local dispatch records it and
|
|
156
|
+
* CI re-derives it (§5). Anything that changes the diff BYTES for identical
|
|
157
|
+
* content therefore produces a blocking `manifest mismatch (stale review)` that
|
|
158
|
+
* no content change explains — so every output-shaping git config knob is
|
|
159
|
+
* neutralised here, explicitly, at its git default:
|
|
160
|
+
*
|
|
161
|
+
* `--no-ext-diff` ← `diff.external`
|
|
162
|
+
* `--no-textconv` ← `diff.<driver>.textconv` attributes
|
|
163
|
+
* `--no-color` ← `color.diff` / `color.ui`, and with it
|
|
164
|
+
* `diff.wsErrorHighlight` / `diff.colorMoved`,
|
|
165
|
+
* which only alter coloured output
|
|
166
|
+
* `--diff-algorithm=myers` ← `diff.algorithm`
|
|
167
|
+
* `--indent-heuristic` ← `diff.indentHeuristic`
|
|
168
|
+
* `--unified=3` ← `diff.context`
|
|
169
|
+
* `--inter-hunk-context=0` ← `diff.interHunkContext`
|
|
170
|
+
* `--src-prefix=a/`,
|
|
171
|
+
* `--dst-prefix=b/` ← `diff.noprefix`, `diff.mnemonicPrefix`
|
|
172
|
+
* `--no-relative` ← `diff.relative` (+ the caller's cwd)
|
|
173
|
+
* `--full-index`,
|
|
174
|
+
* `--abbrev=40` ← `core.abbrev` / `diff.abbrev`: `--full-index`
|
|
175
|
+
* pins the `index <old>..<new>` line, `--abbrev`
|
|
176
|
+
* pins any other abbreviated oid git may print
|
|
177
|
+
* `--submodule=short` ← `diff.submodule`
|
|
178
|
+
* `--ignore-submodules=none` ← `diff.ignoreSubmodules`
|
|
179
|
+
* `-O/dev/null` ← `diff.orderFile` (documented canceller)
|
|
180
|
+
*
|
|
181
|
+
* `--no-renames` (← `diff.renames` / `diff.renameLimit` / `diff.copies`) is the
|
|
182
|
+
* deliberate choice over a pinned `--find-renames=<n>`: rename detection is a
|
|
183
|
+
* similarity HEURISTIC whose result also depends on `diff.renameLimit` and on
|
|
184
|
+
* how many files the diff touches, so even a pinned threshold can flip a rename
|
|
185
|
+
* into an add/delete pair as a branch grows. With renames off the hash depends
|
|
186
|
+
* on content alone.
|
|
187
|
+
*
|
|
188
|
+
* This list is normative (contract §2.0): dropping a flag silently changes every
|
|
189
|
+
* scope hash the suite has ever recorded.
|
|
190
|
+
*/
|
|
191
|
+
export const REVIEW_SCOPE_DIFF_FLAGS: readonly string[] = [
|
|
192
|
+
'--no-ext-diff',
|
|
193
|
+
'--no-textconv',
|
|
194
|
+
'--no-color',
|
|
195
|
+
'--diff-algorithm=myers',
|
|
196
|
+
'--indent-heuristic',
|
|
197
|
+
'--unified=3',
|
|
198
|
+
'--inter-hunk-context=0',
|
|
199
|
+
'--src-prefix=a/',
|
|
200
|
+
'--dst-prefix=b/',
|
|
201
|
+
'--no-relative',
|
|
202
|
+
'--no-renames',
|
|
203
|
+
'--full-index',
|
|
204
|
+
'--abbrev=40',
|
|
205
|
+
'--submodule=short',
|
|
206
|
+
'--ignore-submodules=none',
|
|
207
|
+
'-O/dev/null',
|
|
208
|
+
];
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* The pinned `--name-only` flag set. The changed-file list is config-sensitive
|
|
212
|
+
* too — `diff.renames` decides whether a rename shows one path or two,
|
|
213
|
+
* `diff.orderFile` decides the order, `diff.relative` the prefix — and that list
|
|
214
|
+
* feeds the §2.4 code-path classification, so it is pinned for the same reason.
|
|
215
|
+
*/
|
|
216
|
+
export const REVIEW_SCOPE_NAME_ONLY_FLAGS: readonly string[] = [
|
|
217
|
+
'--name-only',
|
|
218
|
+
'--no-ext-diff',
|
|
219
|
+
'--no-color',
|
|
220
|
+
'--no-relative',
|
|
221
|
+
'--no-renames',
|
|
222
|
+
'-O/dev/null',
|
|
223
|
+
];
|
|
224
|
+
|
|
225
|
+
/** `git diff` argv for the review scope (patch body), config-pinned. */
|
|
226
|
+
export function reviewScopeDiffArgs(base: string): string[] {
|
|
227
|
+
return [
|
|
228
|
+
...REVIEW_SCOPE_GIT_CONFIG,
|
|
229
|
+
'diff',
|
|
230
|
+
...REVIEW_SCOPE_DIFF_FLAGS,
|
|
231
|
+
`${base}...HEAD`,
|
|
232
|
+
'--',
|
|
233
|
+
':/',
|
|
234
|
+
...REVIEW_SCOPE_EXCLUDES,
|
|
235
|
+
];
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** `git diff --name-only` argv for the review scope (changed-file list), config-pinned. */
|
|
239
|
+
export function reviewScopeNameOnlyArgs(base: string): string[] {
|
|
240
|
+
return [
|
|
241
|
+
...REVIEW_SCOPE_GIT_CONFIG,
|
|
242
|
+
'diff',
|
|
243
|
+
...REVIEW_SCOPE_NAME_ONLY_FLAGS,
|
|
244
|
+
`${base}...HEAD`,
|
|
245
|
+
'--',
|
|
246
|
+
':/',
|
|
247
|
+
...REVIEW_SCOPE_EXCLUDES,
|
|
248
|
+
];
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* The artefact directory MUST live under an excluded path (§2.0).
|
|
253
|
+
*
|
|
254
|
+
* The exclusion list is static while the artefact location is a CLI parameter
|
|
255
|
+
* (`--out-dir` / `--artifact-dir`). A directory outside the exclusions puts the
|
|
256
|
+
* findings artifact back inside the reviewed scope, so committing it — which
|
|
257
|
+
* §2.5 requires — invalidates the review it records. That failure is silent, so
|
|
258
|
+
* it is refused loudly here instead: a policy violation, not a warning.
|
|
259
|
+
*
|
|
260
|
+
* Returns an error message, or `null` when the directory is safe.
|
|
261
|
+
*/
|
|
262
|
+
export function scopeExclusionViolation(artifactDirRel: string): string | null {
|
|
263
|
+
const norm = artifactDirRel.split(path.sep).join('/').replace(/^\.\//, '').replace(/\/+$/, '');
|
|
264
|
+
if (path.isAbsolute(norm)) {
|
|
265
|
+
return (
|
|
266
|
+
`❌ --out-dir / --artifact-dir must be repo-relative, got absolute '${artifactDirRel}'.\n` +
|
|
267
|
+
' The review scope excludes repo-relative pathspecs only (contract §2.0).\n'
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
const roots = REVIEW_SCOPE_EXCLUDES.map((s) => s.replace(/^:\(exclude,top\)/, ''));
|
|
271
|
+
const covered = roots.some((r) => norm === r || norm.startsWith(`${r}/`));
|
|
272
|
+
if (covered) {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
return (
|
|
276
|
+
`❌ artefact directory '${artifactDirRel}' is not excluded from the review scope.\n` +
|
|
277
|
+
` Committing a findings artifact there would change the scope hash and invalidate\n` +
|
|
278
|
+
` the review it records (contract §2.0). Excluded roots: ${roots.join(', ')}.\n`
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** A review scope with no reviewable content — the only state `scope none` covers. */
|
|
283
|
+
export function isEmptyScope(scopeDiffText: string): boolean {
|
|
284
|
+
return scopeDiffText.trim() === '';
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export type GitRunner = (args: readonly string[]) => string;
|
|
288
|
+
|
|
289
|
+
export interface ReviewScope {
|
|
290
|
+
/** The review-scope diff body handed to the reviewer. */
|
|
291
|
+
diffText: string;
|
|
292
|
+
/** sha256 of `diffText` — the token the review binds to. */
|
|
293
|
+
hash: string;
|
|
294
|
+
/** Nothing reviewable in scope — the only state a `scope none` skip covers. */
|
|
295
|
+
empty: boolean;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Resolve the review scope in ONE git call. Both the dispatcher and the
|
|
300
|
+
* validator go through this function, injecting their own git wrapper (they
|
|
301
|
+
* differ only in error handling), so the definition of "what a review is bound
|
|
302
|
+
* to" exists exactly once.
|
|
303
|
+
*/
|
|
304
|
+
export function computeReviewScope(runGit: GitRunner, base: string): ReviewScope {
|
|
305
|
+
const diffText = runGit(reviewScopeDiffArgs(base));
|
|
306
|
+
return { diffText, hash: sha256(diffText), empty: isEmptyScope(diffText) };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** Manifest comment block — exactly the contract §5 shape. */
|
|
310
|
+
export function deriveManifest(inputs: ManifestInputs): string {
|
|
311
|
+
return [
|
|
312
|
+
'<!-- context-manifest: v1',
|
|
313
|
+
'inputs:',
|
|
314
|
+
` diff_sha: ${inputs.diffSha}`,
|
|
315
|
+
` scope_hash: ${inputs.scopeHash}`,
|
|
316
|
+
` roadmap: ${inputs.roadmap}`,
|
|
317
|
+
` roadmap_hash: ${inputs.roadmapHash}`,
|
|
318
|
+
` ac_hash: ${inputs.acHash}`,
|
|
319
|
+
'excluded: [session-history, agents/runtime, implementation-context]',
|
|
320
|
+
'tools: [git-diff-branch-scoped, file-read-branch-paths]',
|
|
321
|
+
`dispatched: ${inputs.dispatched}`,
|
|
322
|
+
'-->',
|
|
323
|
+
].join('\n');
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Pure hash derivation — CI re-derivation imports this to verify a submitted
|
|
328
|
+
* artifact's manifest. `null` roadmap/AC text means "not provided" → 'none'.
|
|
329
|
+
* `scopeDiffText` is the REVIEW-SCOPE diff body (see {@link reviewScopeDiffArgs}),
|
|
330
|
+
* never the raw `base...HEAD` diff — the raw diff includes the findings artifact
|
|
331
|
+
* itself and is therefore unverifiable once that artifact is committed.
|
|
332
|
+
*/
|
|
333
|
+
export function expectedHashes(args: {
|
|
334
|
+
scopeDiffText: string;
|
|
335
|
+
roadmapText?: string | null;
|
|
336
|
+
acText?: string | null;
|
|
337
|
+
}): ExpectedHashes {
|
|
338
|
+
return {
|
|
339
|
+
scope_hash: sha256(args.scopeDiffText),
|
|
340
|
+
roadmap_hash: args.roadmapText == null ? 'none' : sha256(args.roadmapText),
|
|
341
|
+
ac_hash: args.acText == null ? 'none' : sha256(args.acText),
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Extract the `## Acceptance Criteria` section — from that heading (inclusive)
|
|
347
|
+
* to the next `## ` heading or EOF. No section → empty string.
|
|
348
|
+
*/
|
|
349
|
+
export function extractAcceptanceCriteria(roadmapText: string): string {
|
|
350
|
+
const lines = roadmapText.split('\n');
|
|
351
|
+
let start = -1;
|
|
352
|
+
for (let i = 0; i < lines.length; i++) {
|
|
353
|
+
if (/^## Acceptance Criteria\s*$/.test(lines[i] as string)) {
|
|
354
|
+
start = i;
|
|
355
|
+
break;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (start === -1) return '';
|
|
359
|
+
let end = lines.length;
|
|
360
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
361
|
+
if (/^## /.test(lines[i] as string)) {
|
|
362
|
+
end = i;
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return lines.slice(start, end).join('\n');
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Parse the context-manifest comment block out of a findings artifact.
|
|
371
|
+
*
|
|
372
|
+
* Line separators are `\r?\n`, not `\n`: the artifact parser in
|
|
373
|
+
* `check_completion_review` already splits that way, and a CRLF working-tree
|
|
374
|
+
* copy (a `core.autocrlf` checkout, an editor that normalises on save) would
|
|
375
|
+
* otherwise yield `null` → a blocking `missing-manifest` violation and exit 1 in
|
|
376
|
+
* `--verify` / `--verify-current`, caused purely by line endings. The captures
|
|
377
|
+
* exclude `\r` for the same reason — a trailing carriage return inside a hash
|
|
378
|
+
* would make every re-derivation mismatch.
|
|
379
|
+
*/
|
|
380
|
+
export function parseManifest(text: string): ParsedManifest | null {
|
|
381
|
+
const nl = '\\r?\\n';
|
|
382
|
+
const re = new RegExp(
|
|
383
|
+
[
|
|
384
|
+
'<!-- context-manifest: v1',
|
|
385
|
+
'inputs:',
|
|
386
|
+
' diff_sha: ([^\\r\\n]+)',
|
|
387
|
+
' scope_hash: ([^\\r\\n]+)',
|
|
388
|
+
' roadmap: ([^\\r\\n]+)',
|
|
389
|
+
' roadmap_hash: ([^\\r\\n]+)',
|
|
390
|
+
' ac_hash: ([^\\r\\n]+)',
|
|
391
|
+
'excluded: \\[session-history, agents/runtime, implementation-context\\]',
|
|
392
|
+
'tools: \\[git-diff-branch-scoped, file-read-branch-paths\\]',
|
|
393
|
+
'dispatched: ([^\\r\\n]+)',
|
|
394
|
+
'-->',
|
|
395
|
+
].join(nl),
|
|
396
|
+
);
|
|
397
|
+
const m = re.exec(text);
|
|
398
|
+
if (!m) return null;
|
|
399
|
+
return {
|
|
400
|
+
diff_sha: m[1] as string,
|
|
401
|
+
scope_hash: m[2] as string,
|
|
402
|
+
roadmap: m[3] as string,
|
|
403
|
+
roadmap_hash: m[4] as string,
|
|
404
|
+
ac_hash: m[5] as string,
|
|
405
|
+
dispatched: m[6] as string,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export function sanitizeSlug(raw: string): string {
|
|
410
|
+
const s = raw
|
|
411
|
+
.toLowerCase()
|
|
412
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
413
|
+
.replace(/^-+|-+$/g, '');
|
|
414
|
+
return s || 'review';
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function git(repo: string, ...args: string[]): string {
|
|
418
|
+
return execFileSync('git', args, {
|
|
419
|
+
cwd: repo,
|
|
420
|
+
encoding: 'utf8',
|
|
421
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
422
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
423
|
+
// cwd decides, never an inherited GIT_DIR (hook environments).
|
|
424
|
+
env: gitEnv(),
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* CI-provided head-branch names, most specific first. `GITHUB_HEAD_REF` is set
|
|
430
|
+
* only on `pull_request` and IS the head branch; `GITHUB_REF_NAME` covers
|
|
431
|
+
* `push`.
|
|
432
|
+
*/
|
|
433
|
+
const CI_BRANCH_ENV_KEYS = ['GITHUB_HEAD_REF', 'GITHUB_REF_NAME'] as const;
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Branch-derived artifact slug. Shared with `check_completion_review`, which
|
|
437
|
+
* uses it to decide whether a leftover artifact in the reviews directory is
|
|
438
|
+
* THIS branch's (and may therefore produce violations) or a foreign one.
|
|
439
|
+
*
|
|
440
|
+
* The CI environment is consulted FIRST because on a `pull_request` checkout
|
|
441
|
+
* `HEAD` is a detached synthetic merge commit: `rev-parse --abbrev-ref HEAD`
|
|
442
|
+
* yields `HEAD`, the slug degrades to `detached-<sha>`, and no artefact can
|
|
443
|
+
* ever be "own" — which inverts contract §2.6 on the layer the contract calls
|
|
444
|
+
* authoritative (an own malformed artefact would be reported as
|
|
445
|
+
* `missing-artifact` instead of the root-cause `bad-marker`, and `stale-review`
|
|
446
|
+
* would never fire in CI). A `HEAD` / `detached-*` env value carries no branch
|
|
447
|
+
* identity either and is ignored.
|
|
448
|
+
*/
|
|
449
|
+
export function deriveSlug(runGit: GitRunner, env: NodeJS.ProcessEnv = process.env): string {
|
|
450
|
+
// GIT FIRST — the env vars are a detached-HEAD fallback, not an override.
|
|
451
|
+
// They describe the WORKFLOW's branch, which is only the right answer when
|
|
452
|
+
// the inspected repo IS the workflow checkout. With an explicit `--repo`
|
|
453
|
+
// pointing elsewhere (every unit test, and any cross-repo invocation) an
|
|
454
|
+
// env-first order returns the outer branch's slug for a foreign repo, so the
|
|
455
|
+
// dispatcher writes one slug and every later lookup asks for another. Git,
|
|
456
|
+
// run against the inspected repo, cannot be wrong about that repo.
|
|
457
|
+
const branch = runGit(['rev-parse', '--abbrev-ref', 'HEAD']).trim();
|
|
458
|
+
if (branch !== '' && branch !== 'HEAD') {
|
|
459
|
+
return sanitizeSlug(branch);
|
|
460
|
+
}
|
|
461
|
+
// Detached HEAD — the case the CI vars exist for (`actions/checkout` leaves
|
|
462
|
+
// a detached HEAD on `pull_request`, where `--abbrev-ref` yields `HEAD`).
|
|
463
|
+
for (const key of CI_BRANCH_ENV_KEYS) {
|
|
464
|
+
const value = (env[key] ?? '').trim();
|
|
465
|
+
if (value !== '' && value !== 'HEAD' && !/^detached-/i.test(value)) {
|
|
466
|
+
return sanitizeSlug(value);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
const short = runGit(['rev-parse', '--short', 'HEAD']).trim();
|
|
470
|
+
return sanitizeSlug(`detached-${short}`);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function deriveSlugFromBranch(repo: string): string {
|
|
474
|
+
return deriveSlug((a) => git(repo, ...a));
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/** ISO timestamp at seconds precision (YYYY-MM-DDTHH:MM:SSZ). */
|
|
478
|
+
function isoSeconds(d: Date): string {
|
|
479
|
+
return d.toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function reviewerPrompt(args: {
|
|
483
|
+
slug: string;
|
|
484
|
+
headSha: string;
|
|
485
|
+
scopeHash: string;
|
|
486
|
+
roadmapGiven: boolean;
|
|
487
|
+
changedFiles: readonly string[];
|
|
488
|
+
}): string {
|
|
489
|
+
const roadmapLine = args.roadmapGiven
|
|
490
|
+
? '- roadmap under review: `roadmap.md` (Acceptance Criteria extracted to `acceptance-criteria.md`)'
|
|
491
|
+
: '- roadmap under review: none (`acceptance-criteria.md` is empty)';
|
|
492
|
+
return [
|
|
493
|
+
`# R2 completion review — ${args.slug}`,
|
|
494
|
+
'',
|
|
495
|
+
'You are a FRESH reviewer subagent. You have no implementation context and',
|
|
496
|
+
'you must not acquire any (blind-review pattern, plan-review-gates.md §5).',
|
|
497
|
+
'',
|
|
498
|
+
'## Review mode',
|
|
499
|
+
'',
|
|
500
|
+
'Senior-engineer review of the branch diff. Search grid — hunt for:',
|
|
501
|
+
'',
|
|
502
|
+
'- errors',
|
|
503
|
+
'- inconsistent logic',
|
|
504
|
+
'- inefficiencies',
|
|
505
|
+
'- bug-producing patterns',
|
|
506
|
+
'',
|
|
507
|
+
'## Rules',
|
|
508
|
+
'',
|
|
509
|
+
'- Review only — write no code, fix nothing.',
|
|
510
|
+
'- Tool allowlist (contract §5): branch-scoped `git diff` + reads of',
|
|
511
|
+
' branch-touched files only; no `git log` beyond the branch, no repo-wide',
|
|
512
|
+
' grep, no reads of `agents/runtime/` or session artifacts.',
|
|
513
|
+
'',
|
|
514
|
+
'## Inputs',
|
|
515
|
+
'',
|
|
516
|
+
`- diff: \`diff.patch\` — the review scope (branch head ${args.headSha}, review`,
|
|
517
|
+
` artefacts excluded), scope hash \`${args.scopeHash}\``,
|
|
518
|
+
roadmapLine,
|
|
519
|
+
'',
|
|
520
|
+
'Changed files:',
|
|
521
|
+
'',
|
|
522
|
+
...args.changedFiles.map((f) => `- ${f}`),
|
|
523
|
+
'',
|
|
524
|
+
'## Output format (contract §2.2)',
|
|
525
|
+
'',
|
|
526
|
+
`Fill the findings table in \`${args.slug}.findings.md\`:`,
|
|
527
|
+
'',
|
|
528
|
+
'```markdown',
|
|
529
|
+
'| # | Severity | File:Line | Finding | Status | Reason/Ref |',
|
|
530
|
+
'|---|----------|-----------|---------|--------|------------|',
|
|
531
|
+
'| 1 | critical | src/x.ts:42 | ... | open | |',
|
|
532
|
+
'```',
|
|
533
|
+
'',
|
|
534
|
+
'- Severity ∈ {`critical`, `high`, `medium`, `low`}, rows sorted descending',
|
|
535
|
+
' by severity (ties keep authoring order).',
|
|
536
|
+
'- Initial status of every finding: `open`.',
|
|
537
|
+
'- 0 findings → replace the table with exactly this honest-null line',
|
|
538
|
+
' (contract §2.3):',
|
|
539
|
+
'',
|
|
540
|
+
'```markdown',
|
|
541
|
+
`**Honest-null:** 0 findings, scope ${args.scopeHash}, reviewed <YYYY-MM-DD>`,
|
|
542
|
+
'```',
|
|
543
|
+
'',
|
|
544
|
+
].join('\n');
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function findingsSkeleton(args: {
|
|
548
|
+
slug: string;
|
|
549
|
+
headSha: string;
|
|
550
|
+
scopeHash: string;
|
|
551
|
+
reviewedDate: string;
|
|
552
|
+
manifest: string;
|
|
553
|
+
}): string {
|
|
554
|
+
return [
|
|
555
|
+
`# Findings: ${args.slug}`,
|
|
556
|
+
`<!-- completion-review: v1 | reviewed: ${args.reviewedDate} | scope: ${args.scopeHash} | diff: ${args.headSha} | reviewer: r2-fresh-subagent-${args.slug} -->`,
|
|
557
|
+
'',
|
|
558
|
+
args.manifest,
|
|
559
|
+
'',
|
|
560
|
+
'| # | Severity | File:Line | Finding | Status | Reason/Ref |',
|
|
561
|
+
'|---|----------|-----------|---------|--------|------------|',
|
|
562
|
+
'<!-- reviewer fills the table; 0 findings => replace the table with the exact honest-null line per docs/contracts/plan-review-gates.md §2.3 -->',
|
|
563
|
+
'',
|
|
564
|
+
].join('\n');
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
interface Args {
|
|
568
|
+
base: string;
|
|
569
|
+
roadmap: string | null;
|
|
570
|
+
slug: string | null;
|
|
571
|
+
outDir: string;
|
|
572
|
+
repo: string;
|
|
573
|
+
printPrompt: boolean;
|
|
574
|
+
format: 'text' | 'json';
|
|
575
|
+
force: boolean;
|
|
576
|
+
now: string | null;
|
|
577
|
+
verify: string | null;
|
|
578
|
+
verifyCurrent: boolean;
|
|
579
|
+
/** `--verify-current` scan root; falls back to `--out-dir`. */
|
|
580
|
+
artifactDir: string | null;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function parse_args(argv: readonly string[]): Args {
|
|
584
|
+
const args: Args = {
|
|
585
|
+
base: 'origin/main',
|
|
586
|
+
roadmap: null,
|
|
587
|
+
slug: null,
|
|
588
|
+
outDir: 'agents/evidence/reviews',
|
|
589
|
+
repo: '.',
|
|
590
|
+
printPrompt: false,
|
|
591
|
+
format: 'text',
|
|
592
|
+
force: false,
|
|
593
|
+
now: null,
|
|
594
|
+
verify: null,
|
|
595
|
+
verifyCurrent: false,
|
|
596
|
+
artifactDir: null,
|
|
597
|
+
};
|
|
598
|
+
const takeValue = (flag: string, v: string | undefined): string => {
|
|
599
|
+
if (v === undefined) {
|
|
600
|
+
process.stderr.write(`dispatch_r2_reviewer: error: argument ${flag}: expected a value\n`);
|
|
601
|
+
process.exit(2);
|
|
602
|
+
}
|
|
603
|
+
return v;
|
|
604
|
+
};
|
|
605
|
+
let i = 0;
|
|
606
|
+
while (i < argv.length) {
|
|
607
|
+
const arg = argv[i] as string;
|
|
608
|
+
if (arg === '--base') {
|
|
609
|
+
args.base = takeValue(arg, argv[++i]);
|
|
610
|
+
} else if (arg === '--roadmap') {
|
|
611
|
+
args.roadmap = takeValue(arg, argv[++i]);
|
|
612
|
+
} else if (arg === '--slug') {
|
|
613
|
+
args.slug = takeValue(arg, argv[++i]);
|
|
614
|
+
} else if (arg === '--out-dir') {
|
|
615
|
+
args.outDir = takeValue(arg, argv[++i]);
|
|
616
|
+
} else if (arg === '--repo') {
|
|
617
|
+
args.repo = takeValue(arg, argv[++i]);
|
|
618
|
+
} else if (arg === '--now') {
|
|
619
|
+
args.now = takeValue(arg, argv[++i]);
|
|
620
|
+
} else if (arg === '--verify') {
|
|
621
|
+
args.verify = takeValue(arg, argv[++i]);
|
|
622
|
+
} else if (arg === '--verify-current') {
|
|
623
|
+
args.verifyCurrent = true;
|
|
624
|
+
} else if (arg === '--artifact-dir') {
|
|
625
|
+
args.artifactDir = takeValue(arg, argv[++i]);
|
|
626
|
+
} else if (arg === '--print-prompt') {
|
|
627
|
+
args.printPrompt = true;
|
|
628
|
+
} else if (arg === '--force') {
|
|
629
|
+
args.force = true;
|
|
630
|
+
} else if (arg === '--format') {
|
|
631
|
+
const v = takeValue(arg, argv[++i]);
|
|
632
|
+
if (v !== 'text' && v !== 'json') {
|
|
633
|
+
process.stderr.write(
|
|
634
|
+
`dispatch_r2_reviewer: error: argument --format: invalid choice: '${v}' (choose from 'text', 'json')\n`,
|
|
635
|
+
);
|
|
636
|
+
process.exit(2);
|
|
637
|
+
}
|
|
638
|
+
args.format = v;
|
|
639
|
+
} else if (arg === '-h' || arg === '--help') {
|
|
640
|
+
process.stdout.write(
|
|
641
|
+
'usage: dispatch_r2_reviewer [-h] [--base REF] [--roadmap PATH] [--slug SLUG]\n' +
|
|
642
|
+
' [--out-dir PATH] [--repo PATH] [--print-prompt]\n' +
|
|
643
|
+
' [--format {text,json}] [--force] [--now ISO]\n' +
|
|
644
|
+
' [--verify FINDINGS_FILE]\n' +
|
|
645
|
+
' [--verify-current [--artifact-dir DIR]]\n',
|
|
646
|
+
);
|
|
647
|
+
process.exit(0);
|
|
648
|
+
} else {
|
|
649
|
+
process.stderr.write(`dispatch_r2_reviewer: error: unrecognized arguments: ${arg}\n`);
|
|
650
|
+
process.exit(2);
|
|
651
|
+
}
|
|
652
|
+
i++;
|
|
653
|
+
}
|
|
654
|
+
return args;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function resolveNow(nowArg: string | null): Date {
|
|
658
|
+
if (nowArg === null) return new Date();
|
|
659
|
+
const d = new Date(nowArg);
|
|
660
|
+
if (Number.isNaN(d.getTime())) {
|
|
661
|
+
throw new Error(`--now: invalid ISO timestamp '${nowArg}'`);
|
|
662
|
+
}
|
|
663
|
+
return d;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Verify one artefact's manifest against the CURRENT repo state.
|
|
668
|
+
*
|
|
669
|
+
* `scopeDiffText` is an optional PRE-COMPUTED review-scope body. The scope is
|
|
670
|
+
* one `git diff` over the whole branch — ~0.5 MB on a branch this size — and it
|
|
671
|
+
* is identical for every artefact in a single pass, so `--verify-current`
|
|
672
|
+
* computes it once and hands it down instead of paying for it per artefact.
|
|
673
|
+
* Omitted (the single-artefact `--verify` path) it is computed here.
|
|
674
|
+
*/
|
|
675
|
+
function runVerify(args: Args, findingsPath: string = args.verify as string, scopeDiffText?: string): number {
|
|
676
|
+
if (completionReviewDisabled(args.repo)) {
|
|
677
|
+
process.stdout.write('⚠️ planning.completion_review=false — R2 manifest verification skipped (settings escape hatch)\n');
|
|
678
|
+
return 0;
|
|
679
|
+
}
|
|
680
|
+
if (!fs.existsSync(findingsPath)) {
|
|
681
|
+
process.stderr.write(`❌ Internal error: findings file not found: ${findingsPath}\n`);
|
|
682
|
+
return 2;
|
|
683
|
+
}
|
|
684
|
+
const manifest = parseManifest(fs.readFileSync(findingsPath, 'utf-8'));
|
|
685
|
+
if (manifest === null) {
|
|
686
|
+
// POLICY, not internal error (contract §5): a manifest is mandatory —
|
|
687
|
+
// "verification, not self-attestation". Exiting 2 here would let every
|
|
688
|
+
// caller warn-and-allow, so omitting the manifest would bypass the
|
|
689
|
+
// whole verification layer.
|
|
690
|
+
process.stderr.write(
|
|
691
|
+
`❌ Policy violation: no context-manifest block found in ${findingsPath} — ` +
|
|
692
|
+
'the §5 manifest is mandatory; a findings artifact without one is unverifiable.\n',
|
|
693
|
+
);
|
|
694
|
+
return 1;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
const scopeText = scopeDiffText ?? computeReviewScope((a) => git(args.repo, ...a), args.base).diffText;
|
|
698
|
+
let roadmapText: string | null = null;
|
|
699
|
+
let acText: string | null = null;
|
|
700
|
+
let roadmapMissing = false;
|
|
701
|
+
if (manifest.roadmap !== 'none') {
|
|
702
|
+
const roadmapPath = path.resolve(args.repo, manifest.roadmap);
|
|
703
|
+
if (fs.existsSync(roadmapPath)) {
|
|
704
|
+
roadmapText = fs.readFileSync(roadmapPath, 'utf-8');
|
|
705
|
+
acText = extractAcceptanceCriteria(roadmapText);
|
|
706
|
+
} else {
|
|
707
|
+
roadmapMissing = true;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
const expected = expectedHashes({ scopeDiffText: scopeText, roadmapText, acText });
|
|
711
|
+
|
|
712
|
+
const diverged: string[] = [];
|
|
713
|
+
const check = (name: string, recorded: string, actual: string): void => {
|
|
714
|
+
if (recorded !== actual) {
|
|
715
|
+
diverged.push(name);
|
|
716
|
+
process.stderr.write(` ${name}: recorded ${recorded} ≠ current ${actual}\n`);
|
|
717
|
+
}
|
|
718
|
+
};
|
|
719
|
+
// `diff_sha` is provenance only and is NEVER compared: committing the
|
|
720
|
+
// artifact (§2.5) and CI's synthetic merge-commit checkout both move HEAD
|
|
721
|
+
// off the recorded sha. Content is what binds.
|
|
722
|
+
check('scope_hash', manifest.scope_hash, expected.scope_hash);
|
|
723
|
+
if (roadmapMissing) {
|
|
724
|
+
diverged.push('roadmap_hash');
|
|
725
|
+
process.stderr.write(` roadmap_hash: recorded ${manifest.roadmap_hash} but roadmap file ${manifest.roadmap} is missing\n`);
|
|
726
|
+
} else {
|
|
727
|
+
check('roadmap_hash', manifest.roadmap_hash, expected.roadmap_hash);
|
|
728
|
+
check('ac_hash', manifest.ac_hash, expected.ac_hash);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
if (diverged.length) {
|
|
732
|
+
process.stderr.write(`❌ manifest mismatch (stale review): ${diverged.join(', ')} diverged\n`);
|
|
733
|
+
return 1;
|
|
734
|
+
}
|
|
735
|
+
process.stdout.write('✅ manifest verified\n');
|
|
736
|
+
return 0;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* `--verify-current`: re-derive the manifest of every artefact RELEVANT to the
|
|
741
|
+
* current review scope.
|
|
742
|
+
*
|
|
743
|
+
* Selection is the whole point of the mode (see the header note). Three
|
|
744
|
+
* deliberate behaviours:
|
|
745
|
+
*
|
|
746
|
+
* - **Foreign artefacts are never verified.** `agents/evidence/reviews/` is
|
|
747
|
+
* tracked and accumulates (§2.6), and every past branch's artefact records
|
|
748
|
+
* a different `scope_hash`, so a verify-everything loop reds by
|
|
749
|
+
* construction and can only be un-stuck by editing an unrelated branch's
|
|
750
|
+
* artefact.
|
|
751
|
+
* - **No relevant artefact → exit 0.** Whether an artefact is *required* is
|
|
752
|
+
* `check_completion_review`'s question (it reports `missing-artifact` /
|
|
753
|
+
* `dead-scan-scope`); a re-derivation step has nothing to re-derive and
|
|
754
|
+
* must not double-report it.
|
|
755
|
+
* - **A bare §2.4 skip declaration is not verified.** It carries no reviewer
|
|
756
|
+
* dispatch and per §5 needs no manifest, so running the manifest check on
|
|
757
|
+
* it would report a policy violation the contract explicitly excludes.
|
|
758
|
+
*/
|
|
759
|
+
function runVerifyCurrent(args: Args): number {
|
|
760
|
+
if (completionReviewDisabled(args.repo)) {
|
|
761
|
+
process.stdout.write('⚠️ planning.completion_review=false — R2 manifest re-derivation skipped (settings escape hatch)\n');
|
|
762
|
+
return 0;
|
|
763
|
+
}
|
|
764
|
+
const artifactDirRel = args.artifactDir ?? args.outDir;
|
|
765
|
+
const excludeViolation = scopeExclusionViolation(artifactDirRel);
|
|
766
|
+
if (excludeViolation !== null) {
|
|
767
|
+
process.stderr.write(excludeViolation);
|
|
768
|
+
return 1;
|
|
769
|
+
}
|
|
770
|
+
const dirAbs = path.resolve(args.repo, artifactDirRel);
|
|
771
|
+
const scope = computeReviewScope((a) => git(args.repo, ...a), args.base);
|
|
772
|
+
|
|
773
|
+
let names: string[] = [];
|
|
774
|
+
try {
|
|
775
|
+
names = fs
|
|
776
|
+
.readdirSync(dirAbs)
|
|
777
|
+
.filter((n) => n.endsWith('.findings.md'))
|
|
778
|
+
.sort();
|
|
779
|
+
} catch {
|
|
780
|
+
// Absent root — a repo with no review corpus yet. Coverage (including a
|
|
781
|
+
// MOVED root) is check_completion_review's dead-scope assertion, not
|
|
782
|
+
// this step's; re-deriving nothing is exit 0.
|
|
783
|
+
names = [];
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
const selected: string[] = [];
|
|
787
|
+
for (const name of names) {
|
|
788
|
+
const abs = path.join(dirAbs, name);
|
|
789
|
+
let text: string;
|
|
790
|
+
try {
|
|
791
|
+
text = fs.readFileSync(abs, 'utf-8');
|
|
792
|
+
} catch (exc) {
|
|
793
|
+
process.stderr.write(
|
|
794
|
+
`❌ Internal error: unreadable artefact ${abs}: ${exc instanceof Error ? exc.message : String(exc)}\n`,
|
|
795
|
+
);
|
|
796
|
+
return 2;
|
|
797
|
+
}
|
|
798
|
+
const rel = artifactRelevance(text, scope.hash, scope.empty);
|
|
799
|
+
if (rel.relevant && rel.carriesReview) {
|
|
800
|
+
selected.push(abs);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
if (selected.length === 0) {
|
|
805
|
+
process.stdout.write(
|
|
806
|
+
`✅ no review-bearing artefact claims the current review scope ${scope.hash} — nothing to re-derive.\n`,
|
|
807
|
+
);
|
|
808
|
+
return 0;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
let mismatched = 0;
|
|
812
|
+
for (const abs of selected) {
|
|
813
|
+
process.stdout.write(`— ${path.relative(path.resolve(args.repo), abs)}\n`);
|
|
814
|
+
// The scope is already in hand — re-deriving it per artefact would run
|
|
815
|
+
// the same whole-branch `git diff` again for an identical result.
|
|
816
|
+
const rc = runVerify(args, abs, scope.diffText);
|
|
817
|
+
if (rc === 2) {
|
|
818
|
+
return 2;
|
|
819
|
+
}
|
|
820
|
+
if (rc !== 0) {
|
|
821
|
+
mismatched += 1;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
if (mismatched > 0) {
|
|
825
|
+
process.stderr.write(
|
|
826
|
+
`❌ ${mismatched} of ${selected.length} relevant artefact(s) failed manifest re-derivation\n`,
|
|
827
|
+
);
|
|
828
|
+
return 1;
|
|
829
|
+
}
|
|
830
|
+
process.stdout.write(`✅ ${selected.length} relevant artefact(s) verified against the current scope\n`);
|
|
831
|
+
return 0;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
function runDispatch(args: Args): number {
|
|
835
|
+
const now = resolveNow(args.now);
|
|
836
|
+
const dispatched = isoSeconds(now);
|
|
837
|
+
const reviewedDate = dispatched.slice(0, 10);
|
|
838
|
+
|
|
839
|
+
const headSha = git(args.repo, 'rev-parse', 'HEAD').trim();
|
|
840
|
+
const scope = computeReviewScope((a) => git(args.repo, ...a), args.base);
|
|
841
|
+
const scopeDiffText = scope.diffText;
|
|
842
|
+
if (scope.empty) {
|
|
843
|
+
process.stderr.write(`❌ Empty diff (${args.base}...HEAD) — nothing to review.\n`);
|
|
844
|
+
return 1;
|
|
845
|
+
}
|
|
846
|
+
const changedFiles = git(args.repo, ...reviewScopeNameOnlyArgs(args.base))
|
|
847
|
+
.split('\n')
|
|
848
|
+
.filter((l) => l.trim() !== '');
|
|
849
|
+
|
|
850
|
+
const slug = args.slug !== null ? sanitizeSlug(args.slug) : deriveSlugFromBranch(args.repo);
|
|
851
|
+
|
|
852
|
+
let roadmapText: string | null = null;
|
|
853
|
+
let acText: string | null = null;
|
|
854
|
+
if (args.roadmap !== null) {
|
|
855
|
+
roadmapText = fs.readFileSync(path.resolve(args.repo, args.roadmap), 'utf-8');
|
|
856
|
+
acText = extractAcceptanceCriteria(roadmapText);
|
|
857
|
+
}
|
|
858
|
+
const hashes = expectedHashes({ scopeDiffText, roadmapText, acText });
|
|
859
|
+
|
|
860
|
+
const outDirViolation = scopeExclusionViolation(args.outDir);
|
|
861
|
+
if (outDirViolation !== null) {
|
|
862
|
+
process.stderr.write(outDirViolation);
|
|
863
|
+
return 1;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
const outDirAbs = path.resolve(args.repo, args.outDir);
|
|
867
|
+
const inputDirAbs = path.join(outDirAbs, `${slug}.review-input`);
|
|
868
|
+
const findingsAbs = path.join(outDirAbs, `${slug}.findings.md`);
|
|
869
|
+
if (fs.existsSync(findingsAbs) && !args.force) {
|
|
870
|
+
process.stderr.write(`❌ Refusing to overwrite existing findings artifact: ${findingsAbs} (use --force)\n`);
|
|
871
|
+
return 1;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
const promptText = reviewerPrompt({
|
|
875
|
+
slug,
|
|
876
|
+
headSha,
|
|
877
|
+
scopeHash: hashes.scope_hash,
|
|
878
|
+
roadmapGiven: roadmapText !== null,
|
|
879
|
+
changedFiles,
|
|
880
|
+
});
|
|
881
|
+
const manifest = deriveManifest({
|
|
882
|
+
diffSha: headSha,
|
|
883
|
+
scopeHash: hashes.scope_hash,
|
|
884
|
+
roadmap: args.roadmap ?? 'none',
|
|
885
|
+
roadmapHash: hashes.roadmap_hash,
|
|
886
|
+
acHash: hashes.ac_hash,
|
|
887
|
+
dispatched,
|
|
888
|
+
});
|
|
889
|
+
const skeleton = findingsSkeleton({ slug, headSha, scopeHash: hashes.scope_hash, reviewedDate, manifest });
|
|
890
|
+
|
|
891
|
+
fs.mkdirSync(inputDirAbs, { recursive: true });
|
|
892
|
+
fs.writeFileSync(path.join(inputDirAbs, 'diff.patch'), scopeDiffText, 'utf-8');
|
|
893
|
+
if (roadmapText !== null) {
|
|
894
|
+
fs.writeFileSync(path.join(inputDirAbs, 'roadmap.md'), roadmapText, 'utf-8');
|
|
895
|
+
}
|
|
896
|
+
fs.writeFileSync(path.join(inputDirAbs, 'acceptance-criteria.md'), acText ?? '', 'utf-8');
|
|
897
|
+
fs.writeFileSync(path.join(inputDirAbs, 'prompt.md'), promptText, 'utf-8');
|
|
898
|
+
fs.writeFileSync(findingsAbs, skeleton, 'utf-8');
|
|
899
|
+
|
|
900
|
+
const inputDirRel = path.join(args.outDir, `${slug}.review-input`);
|
|
901
|
+
const findingsRel = path.join(args.outDir, `${slug}.findings.md`);
|
|
902
|
+
const files = {
|
|
903
|
+
input_dir: inputDirRel,
|
|
904
|
+
diff: path.join(inputDirRel, 'diff.patch'),
|
|
905
|
+
roadmap: roadmapText !== null ? path.join(inputDirRel, 'roadmap.md') : null,
|
|
906
|
+
acceptance_criteria: path.join(inputDirRel, 'acceptance-criteria.md'),
|
|
907
|
+
prompt: path.join(inputDirRel, 'prompt.md'),
|
|
908
|
+
findings: findingsRel,
|
|
909
|
+
};
|
|
910
|
+
|
|
911
|
+
if (args.format === 'json') {
|
|
912
|
+
process.stdout.write(JSON.stringify({ slug, head_sha: headSha, hashes, files }, null, 2) + '\n');
|
|
913
|
+
} else if (args.printPrompt) {
|
|
914
|
+
process.stdout.write(promptText);
|
|
915
|
+
process.stdout.write('\n---\n');
|
|
916
|
+
process.stdout.write(`input package: ${inputDirRel}\n`);
|
|
917
|
+
process.stdout.write(`findings skeleton: ${findingsRel}\n`);
|
|
918
|
+
} else {
|
|
919
|
+
process.stdout.write(
|
|
920
|
+
`✅ R2 reviewer package prepared for '${slug}' (head ${headSha}, scope ${hashes.scope_hash}).\n`,
|
|
921
|
+
);
|
|
922
|
+
process.stdout.write(` input package: ${inputDirRel}\n`);
|
|
923
|
+
process.stdout.write(` findings skeleton: ${findingsRel}\n`);
|
|
924
|
+
process.stdout.write(' Dispatch a FRESH subagent at the input package (never the implementing session).\n');
|
|
925
|
+
}
|
|
926
|
+
return 0;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
export function main(argv?: readonly string[]): number {
|
|
930
|
+
const args = parse_args(argv ?? process.argv.slice(2));
|
|
931
|
+
if (args.verifyCurrent) {
|
|
932
|
+
return runVerifyCurrent(args);
|
|
933
|
+
}
|
|
934
|
+
if (args.verify !== null) {
|
|
935
|
+
return runVerify(args);
|
|
936
|
+
}
|
|
937
|
+
return runDispatch(args);
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
941
|
+
function _isCliEntry(): boolean {
|
|
942
|
+
if (process.argv[1] === undefined) {
|
|
943
|
+
return false;
|
|
944
|
+
}
|
|
945
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
946
|
+
if (import.meta.url === argvUrl) {
|
|
947
|
+
return true;
|
|
948
|
+
}
|
|
949
|
+
// Symlinked invocation (installed projection, macOS /var → /private/var)
|
|
950
|
+
// makes the raw URLs differ; compare realpaths so the guard still fires.
|
|
951
|
+
try {
|
|
952
|
+
const here = fs.realpathSync(fileURLToPath(import.meta.url));
|
|
953
|
+
const argv = fs.realpathSync(path.resolve(process.argv[1]));
|
|
954
|
+
return here === argv;
|
|
955
|
+
} catch {
|
|
956
|
+
return false;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
if (_isCliEntry() || process.argv[1] === _HERE) {
|
|
961
|
+
try {
|
|
962
|
+
process.exit(main());
|
|
963
|
+
} catch (exc) {
|
|
964
|
+
const msg = exc instanceof Error ? exc.message : String(exc);
|
|
965
|
+
process.stderr.write(`❌ Internal error: ${msg}\n`);
|
|
966
|
+
process.exit(2);
|
|
967
|
+
}
|
|
968
|
+
}
|