@event4u/agent-config 9.25.0 → 9.27.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 +58 -143
- package/README.md +1 -1
- package/dist/agent-src/commands/analyze/conformance.md +21 -2
- package/dist/agent-src/commands/review/changes.md +6 -5
- package/dist/agent-src/commands/roadmap/next.md +256 -0
- package/dist/agent-src/commands/roadmap.md +12 -4
- package/dist/agent-src/contexts/execution/subagent-routing.md +4 -1
- package/dist/agent-src/rules/active-remediation.md +2 -0
- package/dist/agent-src/rules/analysis-skill-routing.md +2 -0
- package/dist/agent-src/rules/architecture.md +2 -0
- package/dist/agent-src/rules/artifact-drafting-protocol.md +2 -0
- package/dist/agent-src/rules/artifact-engagement-recording.md +2 -0
- package/dist/agent-src/rules/augment-edit-discipline.md +2 -0
- package/dist/agent-src/rules/autonomous-execution.md +2 -0
- package/dist/agent-src/rules/brand-consistency.md +2 -0
- package/dist/agent-src/rules/brand-source-of-truth.md +2 -0
- package/dist/agent-src/rules/broken-access-control.md +2 -0
- package/dist/agent-src/rules/cli-output-handling.md +2 -0
- package/dist/agent-src/rules/code-comment-discipline.md +2 -0
- package/dist/agent-src/rules/code-provenance.md +2 -0
- package/dist/agent-src/rules/command-suggestion-policy.md +2 -0
- package/dist/agent-src/rules/commit-conventions.md +2 -0
- package/dist/agent-src/rules/communication-through-line.md +2 -0
- package/dist/agent-src/rules/content-quoting-floor.md +2 -0
- package/dist/agent-src/rules/context-hygiene.md +10 -0
- package/dist/agent-src/rules/copilot-routing.md +2 -0
- package/dist/agent-src/rules/cross-source-consistency.md +2 -0
- package/dist/agent-src/rules/decision-revisit-gate.md +2 -0
- package/dist/agent-src/rules/delegation-policy.md +5 -2
- package/dist/agent-src/rules/design-fidelity.md +19 -1
- package/dist/agent-src/rules/design-review-after-ui-write.md +120 -0
- package/dist/agent-src/rules/devcontainer-routing.md +2 -0
- package/dist/agent-src/rules/doc-screenshot-hygiene.md +2 -0
- package/dist/agent-src/rules/docker-commands.md +2 -0
- package/dist/agent-src/rules/domain-adoption-policy.md +2 -0
- package/dist/agent-src/rules/domain-safety-disclaimer.md +2 -0
- package/dist/agent-src/rules/domain-safety-pii.md +2 -0
- package/dist/agent-src/rules/domain-safety-retention.md +2 -0
- package/dist/agent-src/rules/downstream-changes.md +2 -0
- package/dist/agent-src/rules/engineering-safety-floor.md +2 -0
- package/dist/agent-src/rules/evaluator-independence.md +19 -3
- package/dist/agent-src/rules/external-code-graph-interop.md +2 -0
- package/dist/agent-src/rules/external-reference-deep-dive.md +2 -0
- package/dist/agent-src/rules/fast-path-marker-visibility.md +2 -0
- package/dist/agent-src/rules/finance-safety-floor.md +2 -0
- package/dist/agent-src/rules/framework-neutrality-in-generic-skills.md +2 -0
- package/dist/agent-src/rules/git-history-discipline.md +3 -1
- package/dist/agent-src/rules/guidelines.md +2 -0
- package/dist/agent-src/rules/history-discipline.md +2 -0
- package/dist/agent-src/rules/icon-consistency.md +2 -0
- package/dist/agent-src/rules/image-likeness-and-rights.md +2 -0
- package/dist/agent-src/rules/improve-before-implement.md +2 -0
- package/dist/agent-src/rules/invite-challenge.md +2 -0
- package/dist/agent-src/rules/laravel-routing.md +2 -0
- package/dist/agent-src/rules/laravel-translations.md +2 -0
- package/dist/agent-src/rules/legal-safety-floor.md +2 -0
- package/dist/agent-src/rules/lethal-trifecta-guard.md +2 -0
- package/dist/agent-src/rules/linked-projects-onboarding-gate.md +2 -0
- package/dist/agent-src/rules/low-impact-corpus-privacy-floor.md +2 -0
- package/dist/agent-src/rules/markdown-safe-codeblocks.md +2 -0
- package/dist/agent-src/rules/media-governance-routing.md +2 -0
- package/dist/agent-src/rules/media-sync-ground-truth.md +2 -0
- package/dist/agent-src/rules/minimal-safe-diff.md +2 -0
- package/dist/agent-src/rules/missing-tool-handling.md +2 -0
- package/dist/agent-src/rules/model-recommendation.md +2 -0
- package/dist/agent-src/rules/no-attribution-footers.md +2 -0
- package/dist/agent-src/rules/no-decorative-emojis-in-git-surfaces.md +2 -0
- package/dist/agent-src/rules/no-pr-progress-comments.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/onboarding-gate.md +2 -0
- package/dist/agent-src/rules/output-discipline.md +2 -0
- package/dist/agent-src/rules/package-ci-checks.md +2 -0
- package/dist/agent-src/rules/persona-governance.md +2 -0
- package/dist/agent-src/rules/php-coding.md +2 -0
- package/dist/agent-src/rules/prefer-enums-over-literals.md +2 -0
- package/dist/agent-src/rules/preservation-guard.md +2 -0
- package/dist/agent-src/rules/provider-lifecycle-discipline.md +2 -0
- package/dist/agent-src/rules/question-not-instruction.md +2 -0
- package/dist/agent-src/rules/reviewer-awareness.md +2 -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/role-mode-adherence.md +2 -0
- package/dist/agent-src/rules/rule-type-governance.md +2 -0
- package/dist/agent-src/rules/runtime-safety.md +2 -0
- package/dist/agent-src/rules/scale-discipline.md +2 -0
- package/dist/agent-src/rules/secret-vcs-guard.md +2 -0
- package/dist/agent-src/rules/security-sensitive-stop.md +2 -0
- package/dist/agent-src/rules/senior-engineering-discipline.md +2 -0
- package/dist/agent-src/rules/session-canary.md +63 -19
- package/dist/agent-src/rules/settings-ask-protocol.md +180 -0
- package/dist/agent-src/rules/size-enforcement.md +2 -0
- package/dist/agent-src/rules/skill-improvement-trigger.md +2 -0
- package/dist/agent-src/rules/skill-quality.md +2 -0
- package/dist/agent-src/rules/slash-command-routing-policy.md +2 -0
- package/dist/agent-src/rules/source-confidentiality.md +2 -0
- package/dist/agent-src/rules/source-discovery-gate.md +2 -0
- package/dist/agent-src/rules/source-of-truth.md +2 -0
- package/dist/agent-src/rules/spreadsheet-source-quality.md +2 -0
- package/dist/agent-src/rules/strategy-safety-floor.md +2 -0
- package/dist/agent-src/rules/symfony-routing.md +2 -0
- package/dist/agent-src/rules/think-before-action.md +2 -0
- package/dist/agent-src/rules/token-budget-discipline.md +8 -3
- package/dist/agent-src/rules/token-efficiency.md +2 -0
- package/dist/agent-src/rules/token-optimizer-maintenance.md +2 -0
- package/dist/agent-src/rules/tool-safety.md +2 -0
- package/dist/agent-src/rules/ui-audit-gate.md +7 -0
- package/dist/agent-src/rules/untrusted-input-defense.md +2 -0
- package/dist/agent-src/rules/upstream-proposal.md +2 -0
- package/dist/agent-src/rules/user-interaction.md +5 -2
- package/dist/agent-src/rules/user-interrupt-priority.md +2 -0
- package/dist/agent-src/skills/recursive-verification/SKILL.md +13 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli/registry.js +2 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-SJQMJI7M.js → chunk-3KJIMFTB.js} +1 -1
- package/dist/cli-delegate/{chunk-BSLUHV2F.js → chunk-BCPIOAGV.js} +148 -133
- package/dist/cli-delegate/cmd_conformance.js +1 -1
- package/dist/cli-delegate/cmd_doctor.js +1 -1
- package/dist/cli-delegate/cmd_export.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +1 -1
- package/dist/cli-delegate/cmd_route_audit.js +4 -1
- package/dist/cli-delegate/cmd_route_explain.js +4 -1
- package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +1 -1
- package/dist/cli-delegate/cmd_uninstall.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +191 -120
- 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 +11 -8
- package/dist/discovery/trust-report.md +3 -3
- package/dist/discovery/workspaces.json +9 -6
- package/dist/hooks/dispatch.js +1148 -349
- package/dist/install/install.mjs +1 -1
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +571 -417
- package/dist/router.json +1 -1
- package/dist/scripts/condense.js +33 -2
- package/dist/scripts/condense.js.map +1 -1
- package/dist/server/io/yamlIO.js +85 -0
- package/dist/server/io/yamlIO.js.map +1 -1
- package/dist/server/nicknameResolver.js +116 -0
- package/dist/server/nicknameResolver.js.map +1 -0
- package/dist/server/routes/ping.js +29 -11
- package/dist/server/routes/ping.js.map +1 -1
- package/dist/server/routes/settings.js +1 -12
- package/dist/server/routes/settings.js.map +1 -1
- package/dist/server/routes/wizard.js +52 -2
- package/dist/server/routes/wizard.js.map +1 -1
- package/dist/shared/interactiveContext.js +73 -0
- package/dist/shared/interactiveContext.js.map +1 -0
- package/dist/shared/nicknamePrefill.js +59 -0
- package/dist/shared/nicknamePrefill.js.map +1 -0
- package/dist/shared/settingsAsks.js +128 -0
- package/dist/shared/settingsAsks.js.map +1 -0
- package/dist/shared/settingsCarveOut.js +118 -0
- package/dist/shared/settingsCarveOut.js.map +1 -0
- package/dist/shared/settingsConsent.js +93 -0
- package/dist/shared/settingsConsent.js.map +1 -0
- package/dist/shared/settingsSurface.js +7 -1
- package/dist/shared/settingsSurface.js.map +1 -1
- package/dist/ui/assets/index-DmR0mbk4.js.map +1 -1
- package/package.json +1 -1
- package/src/config/agents-paths.yml +15 -0
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +29 -1
- package/src/config/gate-violation-baselines.json +13 -7
- package/src/scripts/_cli/cmd_doctor.ts +6 -21
- package/src/scripts/_cli/cmd_route_audit.ts +4 -1
- package/src/scripts/_cli/cmd_route_explain.ts +9 -1
- package/src/scripts/_dispatch.bash +18 -0
- package/src/scripts/_lib/git_common_dir.ts +128 -0
- package/src/scripts/_lib/obligation_frequency.ts +391 -0
- package/src/scripts/_lib/session_register.ts +361 -0
- package/src/scripts/ai-video/stitch.sh +27 -9
- package/src/scripts/before_complete_hook.ts +23 -4
- package/src/scripts/bench_ab_v2_run.ts +272 -16
- package/src/scripts/bench_ab_v2_stats.ts +156 -3
- package/src/scripts/check_completion_review.ts +80 -3
- package/src/scripts/check_corpus_staleness.ts +692 -0
- package/src/scripts/check_enforcement_coverage.ts +192 -7
- package/src/scripts/check_gate_coverage.ts +67 -0
- package/src/scripts/check_hook_bundle_freshness.ts +183 -0
- package/src/scripts/check_rule_projection_integrity.ts +364 -0
- package/src/scripts/condense.ts +36 -2
- package/src/scripts/conformance_scan.ts +82 -17
- package/src/scripts/dispatch_r2_reviewer.ts +31 -2
- package/src/scripts/generate_settings_reference.ts +232 -0
- package/src/scripts/git_authorization_hook.ts +75 -10
- package/src/scripts/hook_manifest.yaml +87 -27
- package/src/scripts/hooks/block_no_verify.ts +59 -2
- package/src/scripts/hooks/block_unauthorized_git.ts +199 -16
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/hooks/design_slop_hook.ts +13 -1
- package/src/scripts/hooks/dispatch_hook.ts +97 -2
- package/src/scripts/hooks/evidence_independence.ts +40 -6
- package/src/scripts/language_mirror_hook.ts +256 -17
- package/src/scripts/lint_abstraction_thresholds.ts +41 -8
- package/src/scripts/lint_agents_layout.ts +2 -0
- package/src/scripts/lint_hook_manifest.ts +29 -0
- package/src/scripts/memory_learn_hook.ts +109 -6
- package/src/scripts/orchestration_backfill.ts +354 -0
- package/src/scripts/pr_url_reminder_hook.ts +17 -2
- package/src/scripts/release.ts +180 -37
- package/src/scripts/schemas/review-findings.schema.json +99 -0
- package/src/scripts/schemas/rule.schema.json +5 -0
- package/src/scripts/session_canary_hook.ts +35 -3
- package/src/scripts/session_register_hook.ts +288 -0
- package/src/scripts/sessions_cli.ts +140 -0
- package/src/scripts/validate_frontmatter.ts +68 -0
- package/src/server/io/yamlIO.ts +87 -0
- package/src/shared/interactiveContext.ts +98 -0
- package/src/shared/nicknamePrefill.ts +96 -0
- package/src/shared/settingsAsks.ts +176 -0
- package/src/shared/settingsCarveOut.ts +143 -0
- package/src/shared/settingsConsent.ts +136 -0
- package/src/shared/settingsSurface.ts +7 -1
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
#!/usr/bin/env -S npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* Backfill orchestration telemetry from the host transcript corpus.
|
|
4
|
+
*
|
|
5
|
+
* Why this exists (road-to-orchestrator-first-execution Phase 1). Two roadmaps
|
|
6
|
+
* are blocked on "≥20 orchestration lines in the current month", with the
|
|
7
|
+
* stated remedy "use the agent on real delegable work". Measured 2026-08-07:
|
|
8
|
+
* the agent was used on real delegable work **370 times** in this repo over one
|
|
9
|
+
* month, and `orchestration_record` — a model-carried emit step — captured
|
|
10
|
+
* exactly **one** of them. The blocker was never a usage problem; it is an
|
|
11
|
+
* instrumentation defect, and the data it was waiting for is already on disk.
|
|
12
|
+
*
|
|
13
|
+
* What this reads. The host writes one JSONL transcript per session under
|
|
14
|
+
* `~/.claude/projects/<slugified-project-path>/`. An `Agent` dispatch appears
|
|
15
|
+
* as a `tool_use` block; its result appears as a `toolUseResult` object.
|
|
16
|
+
*
|
|
17
|
+
* The coverage limit, which is the point rather than a footnote. Only
|
|
18
|
+
* SYNCHRONOUS dispatches carry cost: `totalTokens`, `totalDurationMs`,
|
|
19
|
+
* `usage`. An ASYNCHRONOUS dispatch returns a launch acknowledgement
|
|
20
|
+
* (`isAsync: true`, `status: "async_launched"`) and its completion never
|
|
21
|
+
* writes cost back into the parent transcript. So the metric-bearing
|
|
22
|
+
* population is a strict subset, and this script reports both counts rather
|
|
23
|
+
* than the flattering one. Emitting 370 lines of which 331 carry a null cost
|
|
24
|
+
* would inflate n against a gate that counts lines.
|
|
25
|
+
*
|
|
26
|
+
* What this deliberately does NOT do. It does not compute a `token_delta`
|
|
27
|
+
* against an in-session baseline. That counterfactual is not on disk and is
|
|
28
|
+
* not measured by anything, so synthesising one here and stamping it
|
|
29
|
+
* `measured` is the exact laundering `orchestration-observed-dispatch-cost`
|
|
30
|
+
* criterion (2) forbids. The orchestrated side is measured; the baseline is a
|
|
31
|
+
* downstream decision that must name its own method.
|
|
32
|
+
*
|
|
33
|
+
* Read-only. Never mutates a transcript.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import { readFileSync, readdirSync, existsSync, writeFileSync } from 'node:fs';
|
|
37
|
+
import { join, basename } from 'node:path';
|
|
38
|
+
import { homedir } from 'node:os';
|
|
39
|
+
|
|
40
|
+
/** One dispatch-family. `unclassified` is a real answer, never a guess. */
|
|
41
|
+
export type DispatchFamily =
|
|
42
|
+
| 'read-only-fanout'
|
|
43
|
+
| 'ordered-steps'
|
|
44
|
+
| 'competitive'
|
|
45
|
+
| 'verdict-judge'
|
|
46
|
+
| 'unclassified';
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* One backfilled dispatch.
|
|
50
|
+
*
|
|
51
|
+
* PII/leak-exclusion by construction: the host's free-text `description` and
|
|
52
|
+
* `prompt` are read to CLASSIFY and are then dropped — no field here can carry
|
|
53
|
+
* free-form content. This is not fastidiousness: the first real run emitted a
|
|
54
|
+
* historical task description carrying a denylisted external-source name and
|
|
55
|
+
* tripped `check_no_external_sources`. A type that cannot hold arbitrary text
|
|
56
|
+
* has no scrubber to fail, which is the same principle
|
|
57
|
+
* `artifact-engagement-recording` states for telemetry events. Never widen
|
|
58
|
+
* this with a `description` / `notes` / `payload` field.
|
|
59
|
+
*/
|
|
60
|
+
export interface BackfilledDispatch {
|
|
61
|
+
session_id: string;
|
|
62
|
+
tool_use_id: string;
|
|
63
|
+
timestamp: string | null;
|
|
64
|
+
subagent_type: string | null;
|
|
65
|
+
family: DispatchFamily;
|
|
66
|
+
family_signal: string;
|
|
67
|
+
resolved_model: string | null;
|
|
68
|
+
/** Measured orchestrated-side cost. `null` on an async dispatch. */
|
|
69
|
+
total_tokens: number | null;
|
|
70
|
+
wall_clock_ms: number | null;
|
|
71
|
+
tool_use_count: number | null;
|
|
72
|
+
/** `measured` only when the host recorded the cost; never inferred. */
|
|
73
|
+
cost_provenance: 'measured' | 'absent';
|
|
74
|
+
async_launch: boolean;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Slugify a project path the way the host names its transcript directory:
|
|
79
|
+
* every character outside `[A-Za-z0-9]` becomes `-`.
|
|
80
|
+
*/
|
|
81
|
+
export function transcriptDirFor(projectPath: string, home = homedir()): string {
|
|
82
|
+
return join(home, '.claude', 'projects', projectPath.replace(/[^A-Za-z0-9]/g, '-'));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Classify a dispatch into a family from OBSERVABLE fields only.
|
|
87
|
+
*
|
|
88
|
+
* The council's round-2 finding was that eight dispatch templates were being
|
|
89
|
+
* treated as one hypothesis. Family is therefore carried per line. Where no
|
|
90
|
+
* enumerated signal matches, the answer is `unclassified` — the same
|
|
91
|
+
* discipline `classifyTask` applies when no delegable signal matches. A
|
|
92
|
+
* classifier that always produces a family would make the per-family split
|
|
93
|
+
* meaningless.
|
|
94
|
+
*/
|
|
95
|
+
export function classifyFamily(
|
|
96
|
+
subagentType: string | null,
|
|
97
|
+
description: string | null,
|
|
98
|
+
prompt: string | null,
|
|
99
|
+
): { family: DispatchFamily; signal: string } {
|
|
100
|
+
const hay = `${description ?? ''}\n${prompt ?? ''}`.toLowerCase();
|
|
101
|
+
|
|
102
|
+
// Verdict/judge first — it is the narrowest and can otherwise be swallowed
|
|
103
|
+
// by the read-only signal, since most verdict passes are also read-only.
|
|
104
|
+
if (subagentType === 'production-validator') {
|
|
105
|
+
return { family: 'verdict-judge', signal: 'subagent_type=production-validator' };
|
|
106
|
+
}
|
|
107
|
+
if (/\b(verify|verdict|judge|adjudicat|audit this|review this|acceptance criterion)\b/.test(hay)) {
|
|
108
|
+
return { family: 'verdict-judge', signal: 'verdict-shaped verb in description/prompt' };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (subagentType === 'Explore') {
|
|
112
|
+
return { family: 'read-only-fanout', signal: 'subagent_type=Explore' };
|
|
113
|
+
}
|
|
114
|
+
if (/\b(read-only|do not edit|never edit|investigation only|map |inventory|locate)\b/.test(hay)) {
|
|
115
|
+
return { family: 'read-only-fanout', signal: 'read-only declaration in prompt' };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (/\b(step \d|then |after (the )?previous|in order|sequentially|depends on step)\b/.test(hay)) {
|
|
119
|
+
return { family: 'ordered-steps', signal: 'ordering language in prompt' };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (/\b(variant|competing|alternative approach|candidate [ab]|independently propose)\b/.test(hay)) {
|
|
123
|
+
return { family: 'competitive', signal: 'competitive framing in prompt' };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return { family: 'unclassified', signal: 'no enumerated family signal matched' };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
interface Extraction {
|
|
130
|
+
dispatches: BackfilledDispatch[];
|
|
131
|
+
sessions_scanned: number;
|
|
132
|
+
unparseable_lines: number;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Walk one project's transcript corpus and pair each `Agent` tool_use with its result. */
|
|
136
|
+
export function extract(dir: string): Extraction {
|
|
137
|
+
if (!existsSync(dir)) {
|
|
138
|
+
return { dispatches: [], sessions_scanned: 0, unparseable_lines: 0 };
|
|
139
|
+
}
|
|
140
|
+
const files = readdirSync(dir).filter((f) => f.endsWith('.jsonl'));
|
|
141
|
+
const dispatches: BackfilledDispatch[] = [];
|
|
142
|
+
let unparseable = 0;
|
|
143
|
+
|
|
144
|
+
for (const file of files) {
|
|
145
|
+
const sessionId = file.replace(/\.jsonl$/, '');
|
|
146
|
+
// tool_use_id → the dispatch we are still waiting on a result for.
|
|
147
|
+
const pending = new Map<string, Partial<BackfilledDispatch> & { prompt: string | null; description: string | null }>();
|
|
148
|
+
const emitted = new Map<string, BackfilledDispatch>();
|
|
149
|
+
|
|
150
|
+
for (const line of readFileSync(join(dir, file), 'utf8').split('\n')) {
|
|
151
|
+
if (line.trim() === '') continue;
|
|
152
|
+
let obj: Record<string, unknown>;
|
|
153
|
+
try {
|
|
154
|
+
obj = JSON.parse(line) as Record<string, unknown>;
|
|
155
|
+
} catch {
|
|
156
|
+
unparseable += 1;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const message = obj.message as Record<string, unknown> | undefined;
|
|
161
|
+
const content = message?.content;
|
|
162
|
+
if (Array.isArray(content)) {
|
|
163
|
+
for (const block of content) {
|
|
164
|
+
const b = block as Record<string, unknown>;
|
|
165
|
+
if (b.type !== 'tool_use' || b.name !== 'Agent') continue;
|
|
166
|
+
const input = (b.input ?? {}) as Record<string, unknown>;
|
|
167
|
+
const id = String(b.id ?? '');
|
|
168
|
+
if (id === '') continue;
|
|
169
|
+
pending.set(id, {
|
|
170
|
+
session_id: sessionId,
|
|
171
|
+
tool_use_id: id,
|
|
172
|
+
timestamp: typeof obj.timestamp === 'string' ? obj.timestamp : null,
|
|
173
|
+
subagent_type: typeof input.subagent_type === 'string' ? input.subagent_type : null,
|
|
174
|
+
description: typeof input.description === 'string' ? input.description : null,
|
|
175
|
+
prompt: typeof input.prompt === 'string' ? input.prompt : null,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const result = obj.toolUseResult as Record<string, unknown> | undefined;
|
|
181
|
+
if (!result || typeof result !== 'object') continue;
|
|
182
|
+
// The result carries no tool_use_id of its own; the host pairs it via
|
|
183
|
+
// the enclosing user message's tool_result block.
|
|
184
|
+
let resultId: string | null = null;
|
|
185
|
+
if (Array.isArray(content)) {
|
|
186
|
+
for (const block of content) {
|
|
187
|
+
const b = block as Record<string, unknown>;
|
|
188
|
+
if (b.type === 'tool_result' && typeof b.tool_use_id === 'string') {
|
|
189
|
+
resultId = b.tool_use_id;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (resultId === null || !pending.has(resultId)) continue;
|
|
194
|
+
const head = pending.get(resultId)!;
|
|
195
|
+
pending.delete(resultId);
|
|
196
|
+
|
|
197
|
+
const totalTokens = typeof result.totalTokens === 'number' ? result.totalTokens : null;
|
|
198
|
+
const { family, signal } = classifyFamily(
|
|
199
|
+
head.subagent_type ?? null,
|
|
200
|
+
head.description ?? null,
|
|
201
|
+
head.prompt,
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
emitted.set(resultId, {
|
|
205
|
+
session_id: sessionId,
|
|
206
|
+
tool_use_id: resultId,
|
|
207
|
+
timestamp: head.timestamp ?? null,
|
|
208
|
+
subagent_type: head.subagent_type ?? null,
|
|
209
|
+
family,
|
|
210
|
+
family_signal: signal,
|
|
211
|
+
resolved_model: typeof result.resolvedModel === 'string' ? result.resolvedModel : null,
|
|
212
|
+
total_tokens: totalTokens,
|
|
213
|
+
wall_clock_ms: typeof result.totalDurationMs === 'number' ? result.totalDurationMs : null,
|
|
214
|
+
tool_use_count: typeof result.totalToolUseCount === 'number' ? result.totalToolUseCount : null,
|
|
215
|
+
cost_provenance: totalTokens === null ? 'absent' : 'measured',
|
|
216
|
+
async_launch: result.isAsync === true,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
dispatches.push(...emitted.values());
|
|
221
|
+
// A dispatch whose result never landed in this transcript (session ended
|
|
222
|
+
// mid-flight) is reported as a gap rather than dropped silently.
|
|
223
|
+
for (const [id, head] of pending) {
|
|
224
|
+
const { family, signal } = classifyFamily(
|
|
225
|
+
head.subagent_type ?? null,
|
|
226
|
+
head.description ?? null,
|
|
227
|
+
head.prompt,
|
|
228
|
+
);
|
|
229
|
+
dispatches.push({
|
|
230
|
+
session_id: sessionId,
|
|
231
|
+
tool_use_id: id,
|
|
232
|
+
timestamp: head.timestamp ?? null,
|
|
233
|
+
subagent_type: head.subagent_type ?? null,
|
|
234
|
+
family,
|
|
235
|
+
family_signal: signal,
|
|
236
|
+
resolved_model: null,
|
|
237
|
+
total_tokens: null,
|
|
238
|
+
wall_clock_ms: null,
|
|
239
|
+
tool_use_count: null,
|
|
240
|
+
cost_provenance: 'absent',
|
|
241
|
+
async_launch: false,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return { dispatches, sessions_scanned: files.length, unparseable_lines: unparseable };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* The shape that actually gets written to disk.
|
|
251
|
+
*
|
|
252
|
+
* `tool_use_id` is pairing plumbing: it exists to match a dispatch to its
|
|
253
|
+
* result during extraction and has no analytical role afterwards. It is
|
|
254
|
+
* dropped at the TYPE level rather than scrubbed at write time, because the
|
|
255
|
+
* host ids are high-entropy opaque tokens and `check_secret_leak` flags them
|
|
256
|
+
* as candidate credentials — correctly, in the sense that it cannot know they
|
|
257
|
+
* are not. Emitting a field only to allow-list it downstream is the worse
|
|
258
|
+
* trade: the artifact does not need it.
|
|
259
|
+
*/
|
|
260
|
+
export type EmittedDispatch = Omit<BackfilledDispatch, 'tool_use_id'>;
|
|
261
|
+
|
|
262
|
+
/** Project an extracted dispatch onto the emitted shape. */
|
|
263
|
+
export function toEmitted(d: BackfilledDispatch): EmittedDispatch {
|
|
264
|
+
const { tool_use_id: _pairingKey, ...rest } = d;
|
|
265
|
+
return rest;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Per-family roll-up. Medians, never means — one 530k outlier should not carry a family. */
|
|
269
|
+
export function summarize(dispatches: BackfilledDispatch[]): Record<string, unknown> {
|
|
270
|
+
const byFamily = new Map<string, number[]>();
|
|
271
|
+
for (const d of dispatches) {
|
|
272
|
+
if (d.cost_provenance !== 'measured' || d.total_tokens === null) continue;
|
|
273
|
+
const bucket = byFamily.get(d.family) ?? [];
|
|
274
|
+
bucket.push(d.total_tokens);
|
|
275
|
+
byFamily.set(d.family, bucket);
|
|
276
|
+
}
|
|
277
|
+
const families: Record<string, unknown> = {};
|
|
278
|
+
for (const [family, tokens] of byFamily) {
|
|
279
|
+
tokens.sort((a, b) => a - b);
|
|
280
|
+
families[family] = {
|
|
281
|
+
n: tokens.length,
|
|
282
|
+
// criterion (1): n<5 is UNDERPOWERED and is never merged to reach n.
|
|
283
|
+
power: tokens.length < 5 ? 'UNDERPOWERED' : 'reportable',
|
|
284
|
+
median_tokens: tokens[Math.floor(tokens.length / 2)],
|
|
285
|
+
min_tokens: tokens[0],
|
|
286
|
+
max_tokens: tokens[tokens.length - 1],
|
|
287
|
+
sum_tokens: tokens.reduce((a, b) => a + b, 0),
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
const measured = dispatches.filter((d) => d.cost_provenance === 'measured').length;
|
|
291
|
+
return {
|
|
292
|
+
dispatches_total: dispatches.length,
|
|
293
|
+
dispatches_with_measured_cost: measured,
|
|
294
|
+
dispatches_without_cost: dispatches.length - measured,
|
|
295
|
+
async_launches: dispatches.filter((d) => d.async_launch).length,
|
|
296
|
+
cost_coverage_ratio: dispatches.length === 0 ? 0 : Number((measured / dispatches.length).toFixed(4)),
|
|
297
|
+
families,
|
|
298
|
+
models: dispatches
|
|
299
|
+
.filter((d) => d.cost_provenance === 'measured')
|
|
300
|
+
.reduce<Record<string, number>>((acc, d) => {
|
|
301
|
+
const key = d.resolved_model ?? 'unknown';
|
|
302
|
+
acc[key] = (acc[key] ?? 0) + 1;
|
|
303
|
+
return acc;
|
|
304
|
+
}, {}),
|
|
305
|
+
baseline_note:
|
|
306
|
+
'No in-session counterfactual exists on disk. token_delta is NOT computed here; ' +
|
|
307
|
+
'any downstream comparison must name its own baseline method and stamp it estimated.',
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function main(argv: string[]): number {
|
|
312
|
+
const projectArg = argv.indexOf('--project');
|
|
313
|
+
const project =
|
|
314
|
+
projectArg >= 0 && argv[projectArg + 1] !== undefined ? argv[projectArg + 1]! : process.cwd();
|
|
315
|
+
const outArg = argv.indexOf('--out');
|
|
316
|
+
const dir = transcriptDirFor(project);
|
|
317
|
+
|
|
318
|
+
if (!existsSync(dir)) {
|
|
319
|
+
process.stderr.write(`orchestration_backfill: no transcript corpus at ${dir}\n`);
|
|
320
|
+
return 1;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const { dispatches, sessions_scanned, unparseable_lines } = extract(dir);
|
|
324
|
+
const summary = summarize(dispatches);
|
|
325
|
+
// Emit the corpus identity, never the absolute path: the transcript
|
|
326
|
+
// directory is rooted at the operator's home and a committed report
|
|
327
|
+
// carrying it leaks an identity-revealing local path for no analytical
|
|
328
|
+
// gain (the basename already identifies which corpus was read).
|
|
329
|
+
const report = {
|
|
330
|
+
schema_version: 1,
|
|
331
|
+
source_corpus: basename(dir),
|
|
332
|
+
sessions_scanned,
|
|
333
|
+
unparseable_lines,
|
|
334
|
+
...summary,
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
338
|
+
|
|
339
|
+
if (outArg >= 0 && argv[outArg + 1] !== undefined) {
|
|
340
|
+
const lines = dispatches
|
|
341
|
+
.filter((d) => d.cost_provenance === 'measured')
|
|
342
|
+
.map((d) => JSON.stringify({ input_kind: 'orchestration', orchestration: toEmitted(d) }))
|
|
343
|
+
.join('\n');
|
|
344
|
+
writeFileSync(argv[outArg + 1]!, lines === '' ? '' : `${lines}\n`, 'utf8');
|
|
345
|
+
process.stderr.write(
|
|
346
|
+
`orchestration_backfill: wrote ${summary.dispatches_with_measured_cost} measured line(s) to ${argv[outArg + 1]}\n`,
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
return 0;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (process.argv[1] !== undefined && process.argv[1].includes('orchestration_backfill')) {
|
|
353
|
+
process.exit(main(process.argv.slice(2)));
|
|
354
|
+
}
|
|
@@ -66,6 +66,20 @@ function _isObject(v: unknown): v is JsonObject {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
/** Return [tool_name, command_text] from a tool-event payload. */
|
|
69
|
+
/**
|
|
70
|
+
* Unwrap the dispatcher envelope.
|
|
71
|
+
*
|
|
72
|
+
* The dispatcher hands every concern `{event, platform, payload: <native>}`,
|
|
73
|
+
* but this hook read `tool_name` / `tool_response` off the TOP level. Fed a raw
|
|
74
|
+
* payload it worked; fed the real envelope it found nothing and returned ALLOW.
|
|
75
|
+
* Measured 2026-08-06: standalone 624 bytes of output, through the built
|
|
76
|
+
* dispatcher 0 bytes — the reminder had never fired in production.
|
|
77
|
+
*/
|
|
78
|
+
function _unwrap(envelope: JsonObject): JsonObject {
|
|
79
|
+
const inner = envelope["payload"];
|
|
80
|
+
return _isObject(inner) ? inner : envelope;
|
|
81
|
+
}
|
|
82
|
+
|
|
69
83
|
function _extractCommand(payload: JsonObject): [string | null, string | null] {
|
|
70
84
|
const toolRaw = payload["tool_name"] ?? payload["toolName"] ?? payload["tool"];
|
|
71
85
|
const tool = typeof toolRaw === "string" ? toolRaw : null;
|
|
@@ -124,12 +138,13 @@ export function main(): number {
|
|
|
124
138
|
return EXIT_ALLOW;
|
|
125
139
|
}
|
|
126
140
|
|
|
127
|
-
const
|
|
141
|
+
const inner = _unwrap(envelope);
|
|
142
|
+
const [tool, command] = _extractCommand(inner);
|
|
128
143
|
if (!_isPrCreate(tool, command)) {
|
|
129
144
|
return EXIT_ALLOW;
|
|
130
145
|
}
|
|
131
146
|
|
|
132
|
-
const match = _PR_URL.exec(_toolOutput(
|
|
147
|
+
const match = _PR_URL.exec(_toolOutput(inner));
|
|
133
148
|
if (match === null) {
|
|
134
149
|
return EXIT_ALLOW; // create attempted but no URL produced (failed / dry-run) — do not nag
|
|
135
150
|
}
|
package/src/scripts/release.ts
CHANGED
|
@@ -700,13 +700,84 @@ export function _failed_checks_report(names: readonly string[]): string {
|
|
|
700
700
|
return lines.join('\n');
|
|
701
701
|
}
|
|
702
702
|
|
|
703
|
+
/** Rounds of re-watching before an absent required check is called absent. */
|
|
704
|
+
const _NO_CHECKS_ROUNDS = 10;
|
|
705
|
+
/** Delay between those rounds — 10 × 60 s ≈ 10 minutes of tolerance. */
|
|
706
|
+
const _NO_CHECKS_DELAY_MS = 60_000;
|
|
707
|
+
|
|
703
708
|
/**
|
|
704
|
-
*
|
|
709
|
+
* Required status-check contexts for a branch, parsed from
|
|
710
|
+
* `gh api repos/{owner}/{repo}/rules/branches/{branch}`.
|
|
711
|
+
*
|
|
712
|
+
* That endpoint is the one that answers the question here: this repo protects
|
|
713
|
+
* `main` with a RULESET, and the classic `/branches/main/protection` API
|
|
714
|
+
* answers "Branch not protected" for it — so a check built on the classic
|
|
715
|
+
* endpoint would conclude "no required checks" and wave everything through.
|
|
716
|
+
*
|
|
717
|
+
* Any parse or shape surprise yields [] — the caller then behaves exactly as
|
|
718
|
+
* it did before this function existed.
|
|
719
|
+
*/
|
|
720
|
+
export function _required_contexts_from_rules(json: string): string[] {
|
|
721
|
+
let parsed: unknown;
|
|
722
|
+
try {
|
|
723
|
+
parsed = JSON.parse(json);
|
|
724
|
+
} catch {
|
|
725
|
+
return [];
|
|
726
|
+
}
|
|
727
|
+
if (!Array.isArray(parsed)) return [];
|
|
728
|
+
const out: string[] = [];
|
|
729
|
+
for (const rule of parsed) {
|
|
730
|
+
const r = rule as { type?: unknown; parameters?: { required_status_checks?: unknown } };
|
|
731
|
+
if (r.type !== 'required_status_checks') continue;
|
|
732
|
+
const checks = r.parameters?.required_status_checks;
|
|
733
|
+
if (!Array.isArray(checks)) continue;
|
|
734
|
+
for (const c of checks) {
|
|
735
|
+
const ctx = (c as { context?: unknown }).context;
|
|
736
|
+
if (typeof ctx === 'string' && ctx !== '') out.push(ctx);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
return out;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
export type NoChecksAction = 'retry' | 'accept' | 'die';
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* What to do when `gh pr checks` reports no checks at all.
|
|
746
|
+
*
|
|
747
|
+
* The old code had one answer — accept — for two situations it could not tell
|
|
748
|
+
* apart: a repo that configures no required checks (nothing will ever arrive,
|
|
749
|
+
* accepting is correct) and a repo whose checks have simply not been
|
|
750
|
+
* dispatched yet (accepting merges, tags and publishes unvalidated).
|
|
751
|
+
*
|
|
752
|
+
* The branch's own protection rules separate them. Required contexts declared
|
|
753
|
+
* ⇒ something IS expected, so absence means not-yet, and the answer is to wait
|
|
754
|
+
* and finally to refuse. None declared ⇒ the pre-existing tolerance stands.
|
|
755
|
+
*/
|
|
756
|
+
export function _no_checks_action(
|
|
757
|
+
required: readonly string[],
|
|
758
|
+
round: number,
|
|
759
|
+
maxRounds: number = _NO_CHECKS_ROUNDS,
|
|
760
|
+
): NoChecksAction {
|
|
761
|
+
if (required.length === 0) return 'accept';
|
|
762
|
+
return round < maxRounds ? 'retry' : 'die';
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* Watch PR checks and tolerate the 'no checks' case ONLY where it is tolerable.
|
|
705
767
|
*
|
|
706
768
|
* `gh pr checks --watch` exits 1 both on real failures and when no checks are
|
|
707
|
-
* reported at all
|
|
708
|
-
*
|
|
709
|
-
*
|
|
769
|
+
* reported at all. Real failures still die.
|
|
770
|
+
*
|
|
771
|
+
* Measured 2026-08-06/07: during a critical GitHub Actions incident
|
|
772
|
+
* (15:22–02:04 UTC, webhook triggers throttled from 20:34) `pull_request`
|
|
773
|
+
* events were delivered 15–30 minutes late. The five-second grace period below
|
|
774
|
+
* expires long before that, so the old unconditional accept would have tagged
|
|
775
|
+
* `main` and published to npm without a single check having run. It did not
|
|
776
|
+
* only because `main` happened to be independently green.
|
|
777
|
+
*
|
|
778
|
+
* So the tolerance is now conditional on the branch declaring no required
|
|
779
|
+
* checks. Where it declares some, absence is treated as latency: re-watch, and
|
|
780
|
+
* refuse rather than release blind.
|
|
710
781
|
*
|
|
711
782
|
* A short grace period gives GitHub time to register workflow runs on a
|
|
712
783
|
* freshly-pushed branch.
|
|
@@ -721,41 +792,76 @@ function watch_pr_checks(branch: string): void {
|
|
|
721
792
|
// busy-wait stand-in for time.sleep(5) without an event-loop yield.
|
|
722
793
|
}
|
|
723
794
|
}
|
|
724
|
-
//
|
|
725
|
-
//
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
795
|
+
// Rounds exist only for the no-checks-yet case below; every other outcome
|
|
796
|
+
// returns or dies on the first pass, exactly as before.
|
|
797
|
+
for (let round = 0; ; round++) {
|
|
798
|
+
// The branch is passed explicitly — `gh` inferring the PR from HEAD
|
|
799
|
+
// resolved to `main` mid-run once (2026-08-03, 9.16.0 resume) and the
|
|
800
|
+
// whole release died on "no pull requests found for branch main".
|
|
801
|
+
const proc = run(['gh', 'pr', 'checks', branch, '--watch'], {
|
|
802
|
+
check: false,
|
|
803
|
+
capture: true,
|
|
804
|
+
});
|
|
805
|
+
const output = ((proc.stdout || '') + (proc.stderr || '')).trim();
|
|
806
|
+
const returncode = proc.returncode;
|
|
807
|
+
if (returncode === 0) {
|
|
808
|
+
if (output) {
|
|
809
|
+
process.stdout.write(output + '\n');
|
|
810
|
+
}
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
if (output.toLowerCase().includes('no checks reported')) {
|
|
814
|
+
const rules = run(
|
|
815
|
+
['gh', 'api', `repos/{owner}/{repo}/rules/branches/${MAIN_BRANCH}`],
|
|
816
|
+
{ check: false, capture: true },
|
|
817
|
+
);
|
|
818
|
+
const required = _required_contexts_from_rules(rules.stdout ?? '');
|
|
819
|
+
const action = _no_checks_action(required, round, _NO_CHECKS_ROUNDS);
|
|
820
|
+
|
|
821
|
+
if (action === 'accept') {
|
|
822
|
+
process.stdout.write(`⚠️ ${output}\n`);
|
|
823
|
+
process.stdout.write(
|
|
824
|
+
' Continuing without check validation — configure required ' +
|
|
825
|
+
'checks in branch protection to enforce this gate.\n',
|
|
826
|
+
);
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
if (action === 'retry') {
|
|
830
|
+
process.stdout.write(
|
|
831
|
+
`⏳ no checks reported yet, but ${MAIN_BRANCH} requires ` +
|
|
832
|
+
`${String(required.length)} (${required.join(', ')}) — ` +
|
|
833
|
+
`waiting (${String(round + 1)}/${String(_NO_CHECKS_ROUNDS)})\n`,
|
|
834
|
+
);
|
|
835
|
+
if (_exec_override === null) {
|
|
836
|
+
const until = Date.now() + _NO_CHECKS_DELAY_MS;
|
|
837
|
+
while (Date.now() < until) {
|
|
838
|
+
// blocking wait, same shape as the grace period above
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
continue;
|
|
842
|
+
}
|
|
843
|
+
die(
|
|
844
|
+
`no checks reported for ${branch} after ` +
|
|
845
|
+
`${String(_NO_CHECKS_ROUNDS)} rounds, but ${MAIN_BRANCH} requires ` +
|
|
846
|
+
`${required.join(', ')}. Refusing to merge, tag and publish ` +
|
|
847
|
+
'unvalidated — GitHub may be delaying event delivery (check ' +
|
|
848
|
+
'githubstatus.com). Re-run with `task release -- --resume --yes` ' +
|
|
849
|
+
'once the checks appear.',
|
|
850
|
+
);
|
|
851
|
+
}
|
|
734
852
|
if (output) {
|
|
735
|
-
process.
|
|
853
|
+
process.stderr.write(output + '\n');
|
|
736
854
|
}
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
}
|
|
747
|
-
if (output) {
|
|
748
|
-
process.stderr.write(output + '\n');
|
|
749
|
-
}
|
|
750
|
-
const summary = run(['gh', 'pr', 'checks', branch, '--json', 'name,bucket'], {
|
|
751
|
-
check: false,
|
|
752
|
-
capture: true,
|
|
753
|
-
});
|
|
754
|
-
const report = _failed_checks_report(_failed_check_names(summary.stdout ?? ''));
|
|
755
|
-
if (report) {
|
|
756
|
-
process.stderr.write(report + '\n');
|
|
855
|
+
const summary = run(['gh', 'pr', 'checks', branch, '--json', 'name,bucket'], {
|
|
856
|
+
check: false,
|
|
857
|
+
capture: true,
|
|
858
|
+
});
|
|
859
|
+
const report = _failed_checks_report(_failed_check_names(summary.stdout ?? ''));
|
|
860
|
+
if (report) {
|
|
861
|
+
process.stderr.write(report + '\n');
|
|
862
|
+
}
|
|
863
|
+
die(`PR checks failed (exit ${returncode})`);
|
|
757
864
|
}
|
|
758
|
-
die(`PR checks failed (exit ${returncode})`);
|
|
759
865
|
}
|
|
760
866
|
|
|
761
867
|
/**
|
|
@@ -767,11 +873,48 @@ function watch_pr_checks(branch: string): void {
|
|
|
767
873
|
* 2026-08-03, 9.15.0). Same-branch commits are never ours to drop, so the
|
|
768
874
|
* recovery is merge-and-retry, never force-push.
|
|
769
875
|
*/
|
|
876
|
+
/**
|
|
877
|
+
* True when git rejected the push because the remote ref moved ahead — the
|
|
878
|
+
* ONLY failure the merge-and-retry recovery below can repair.
|
|
879
|
+
*
|
|
880
|
+
* git's wording for this is stable across versions: the hint is `fetch first`
|
|
881
|
+
* / `Updates were rejected`, and the ref line carries `[rejected]` with
|
|
882
|
+
* `non-fast-forward` or `fetch first`.
|
|
883
|
+
*/
|
|
884
|
+
export function _is_non_fast_forward(stderr: string, stdout: string): boolean {
|
|
885
|
+
const text = `${stderr}\n${stdout}`;
|
|
886
|
+
return (
|
|
887
|
+
/\[rejected\]/i.test(text) &&
|
|
888
|
+
/(non-fast-forward|fetch first|behind its remote counterpart)/i.test(text)
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
|
|
770
892
|
function push_release_branch(branch: string): void {
|
|
771
|
-
const first = run(['git', 'push', '-u', REMOTE, branch], { check: false });
|
|
893
|
+
const first = run(['git', 'push', '-u', REMOTE, branch], { check: false, capture: true });
|
|
772
894
|
if (first.returncode === 0) {
|
|
895
|
+
process.stdout.write(first.stdout);
|
|
896
|
+
process.stderr.write(first.stderr);
|
|
773
897
|
return;
|
|
774
898
|
}
|
|
899
|
+
|
|
900
|
+
// A push can fail for many reasons — a pre-push hook refusing the commit,
|
|
901
|
+
// no credentials, a protected branch. Only ONE of them is repaired by
|
|
902
|
+
// fetch+merge+retry, and the recovery's first step (`git fetch <branch>`)
|
|
903
|
+
// exits 128 for a branch that was never pushed. Taking that path
|
|
904
|
+
// unconditionally replaced every real error with
|
|
905
|
+
// `couldn't find remote ref <branch>` — measured 2026-08-06 on 9.26.0,
|
|
906
|
+
// where a pre-push gate rejection surfaced as a git-fetch crash and cost
|
|
907
|
+
// an hour of diagnosis pointed at the wrong layer.
|
|
908
|
+
if (!_is_non_fast_forward(first.stderr, first.stdout)) {
|
|
909
|
+
process.stdout.write(first.stdout);
|
|
910
|
+
process.stderr.write(first.stderr);
|
|
911
|
+
die(
|
|
912
|
+
`push of ${branch} failed (exit ${first.returncode}) and the remote ref did not move — ` +
|
|
913
|
+
'this is not the fetch-and-retry case. The push output above is the real error ' +
|
|
914
|
+
'(a pre-push gate, credentials, or branch protection); fix that and re-run.',
|
|
915
|
+
);
|
|
916
|
+
}
|
|
917
|
+
|
|
775
918
|
process.stdout.write(`↻ ${REMOTE}/${branch} moved — integrating and retrying push\n`);
|
|
776
919
|
run(['git', 'fetch', REMOTE, branch]);
|
|
777
920
|
run(['git', 'merge', '--no-edit', `${REMOTE}/${branch}`]);
|