@event4u/agent-config 14.14.0 → 14.15.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/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +63 -0
- package/README.md +1 -1
- package/dist/agent-src/commands/pr/create.md +16 -1
- package/dist/agent-src/contexts/execution/terminal-states.md +25 -4
- package/dist/agent-src/rules/missing-skill-recovery.md +2 -2
- package/dist/agent-src/rules/token-budget-discipline.md +2 -2
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli/registry.js +1 -0
- package/dist/cli-delegate/{chunk-ILMUPLGW.js → chunk-7RXIKF62.js} +159 -86
- package/dist/cli-delegate/cmd_conformance.js +513 -17
- package/dist/cli-delegate/cmd_doctor.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +5 -5
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/dormancy-report.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 +1043 -390
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +703 -115
- package/package.json +1 -1
- package/src/config/assurance-capability-registry.json +1 -1
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +62 -2
- package/src/config/gate-reachability-exemptions.json +1 -0
- package/src/config/gate-violation-baselines.json +4 -3
- package/src/config/rule-enforcement-baseline.json +0 -1
- package/src/config/web-launch-readiness.json +4 -2
- package/src/scripts/_cli/_doctor_emit.ts +71 -0
- package/src/scripts/_cli/_doctor_strict.ts +163 -0
- package/src/scripts/_cli/cmd_doctor.ts +16 -37
- package/src/scripts/_dispatch.bash +11 -0
- package/src/scripts/_lib/branch_convergence.ts +186 -0
- package/src/scripts/_lib/context_observation.ts +132 -0
- package/src/scripts/_lib/continuation_ladder.ts +188 -0
- package/src/scripts/_lib/experiment_binding.ts +95 -0
- package/src/scripts/_lib/ignored_blocker.ts +8 -1
- package/src/scripts/_lib/outcome_envelope.ts +6 -1
- package/src/scripts/_lib/outcome_vocabularies.ts +135 -5
- package/src/scripts/_lib/release_highlights.ts +75 -10
- package/src/scripts/_lib/repeated_failure.ts +19 -2
- package/src/scripts/_lib/runtime_journal.ts +11 -4
- package/src/scripts/check_agent_artifact_location.ts +337 -0
- package/src/scripts/check_artefact_count_messaging.ts +74 -8
- package/src/scripts/check_enforcement_coverage.ts +40 -1
- package/src/scripts/check_release_highlights.ts +17 -1
- package/src/scripts/check_web_launch_readiness.ts +310 -20
- package/src/scripts/evolution_lab.ts +53 -1
- package/src/scripts/generate_capability_matrix.ts +16 -9
- package/src/scripts/git_authorization_hook.ts +476 -6
- package/src/scripts/grade_target_readiness.ts +221 -2
- package/src/scripts/hook_effect_doctor.ts +47 -4
- package/src/scripts/hooks/block_unauthorized_git.ts +238 -9
- package/src/scripts/hooks/merge_impact.ts +207 -0
- package/src/scripts/hooks/run_continuation_hook.ts +78 -88
- package/src/scripts/hooks/session_eol_hook.ts +12 -1
- package/src/scripts/install-hooks.sh +18 -0
- package/src/scripts/lint_code_comments.ts +106 -1
- package/src/scripts/release.ts +5 -17
- package/src/scripts/release_drill.ts +76 -1
- package/src/scripts/release_env.ts +31 -0
- package/src/scripts/release_publication.ts +125 -1
- package/src/scripts/roadmap_context.ts +15 -0
- package/src/scripts/skill_scout.ts +633 -0
- package/src/scripts/sync_pr_branch.ts +521 -42
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
frontmatterPaths,
|
|
34
34
|
proseEqual,
|
|
35
35
|
splitFrontmatter
|
|
36
|
-
} from "./chunk-
|
|
36
|
+
} from "./chunk-7RXIKF62.js";
|
|
37
37
|
import "./chunk-FTAISGDQ.js";
|
|
38
38
|
import "./chunk-M2DPGFZ4.js";
|
|
39
39
|
import "./chunk-BTWUYDGN.js";
|
|
@@ -761,12 +761,39 @@ var ALL_OPS = [
|
|
|
761
761
|
"pr-merge-auto",
|
|
762
762
|
"tag",
|
|
763
763
|
"release",
|
|
764
|
-
"publish"
|
|
764
|
+
"publish",
|
|
765
|
+
"unpublish",
|
|
766
|
+
"deprecate",
|
|
767
|
+
"release-asset",
|
|
768
|
+
"protection",
|
|
769
|
+
"workflow-toggle",
|
|
770
|
+
"repo-lifecycle",
|
|
771
|
+
"review-approve",
|
|
772
|
+
"force-push",
|
|
773
|
+
"worktree-remove",
|
|
774
|
+
"clean-ignored",
|
|
775
|
+
"tag-force",
|
|
776
|
+
"rebase",
|
|
777
|
+
"reset-hard",
|
|
778
|
+
"clean",
|
|
779
|
+
"stash-drop",
|
|
780
|
+
"branch-delete",
|
|
781
|
+
"close"
|
|
765
782
|
];
|
|
766
783
|
var PHRASES = [
|
|
767
784
|
{ op: "commit", re: /\b(commit(e|et|te|ten)?|committe|einchecken)\b/i },
|
|
768
785
|
{ op: "push", re: /\b(push(e|en|t)?|hochladen|hochschieben|raufschieben)\b/i },
|
|
769
|
-
|
|
786
|
+
// A CREATION VERB IS REQUIRED, as for `pr-create` above and for the same
|
|
787
|
+
// reason. The bare noun could not tell an authorization from a mention, and
|
|
788
|
+
// the mention is the dangerous half: "den branch nicht löschen" names the
|
|
789
|
+
// object precisely in order to forbid acting on it, yet authorized branch
|
|
790
|
+
// creation — the negation trails a bare-noun match, so neither the
|
|
791
|
+
// look-behind nor the span check in `negatedBefore` can reach it. Widened
|
|
792
|
+
// 2026-09-03 alongside the destructive-coverage work that measured it.
|
|
793
|
+
{
|
|
794
|
+
op: "branch",
|
|
795
|
+
re: /\b(erstell(e|en)?|mach(e|en)?|leg(e|en)?\s+an|anleg(e|en)?|(er|be)?(ö|oe)ffne|open|create|new|neuer?|starte?|checkout|wechsl?e|switch)\b[^.\n]{0,30}\b(branch|feature-branch|zweig)\b|\b(branch|feature-branch|zweig)\b[^.\n]{0,25}\b(erstellen|anlegen|aufmachen|(ö|oe)ffnen|starten)\b/i
|
|
796
|
+
},
|
|
770
797
|
// A creation verb is required — "schau dir den PR an" is not "open a PR".
|
|
771
798
|
{
|
|
772
799
|
op: "pr-create",
|
|
@@ -803,7 +830,111 @@ var PHRASES = [
|
|
|
803
830
|
op: "release",
|
|
804
831
|
re: /\b(releasen?|ver(ö|oe)ffentlich(e|en))\b(?!\s*[- ]?(notes?|branch|candidate|pr\b|datum|date|zweig))/i
|
|
805
832
|
},
|
|
806
|
-
{ op: "publish", re: /\b(publish(e|en)?|publiziere[n]?)\b/i }
|
|
833
|
+
{ op: "publish", re: /\b(publish(e|en)?|publiziere[n]?)\b/i },
|
|
834
|
+
// ── Authorization vocabulary for the ops added 2026-09-03.
|
|
835
|
+
//
|
|
836
|
+
// NON-NEGOTIABLE, and the reason it ships in the same change as the new BLOCK
|
|
837
|
+
// ops: a blocked operation with no phrase that authorizes it is a dead end.
|
|
838
|
+
// The guard refuses, the user says "doch, mach das", the classifier records
|
|
839
|
+
// nothing, and the refusal repeats forever. Every op the guard can block must
|
|
840
|
+
// have a sentence that unblocks it, or the user's override is a claim rather
|
|
841
|
+
// than a mechanism.
|
|
842
|
+
//
|
|
843
|
+
// Tight on purpose. Under-matching costs one confirmation; over-matching
|
|
844
|
+
// authorizes something the user did not ask for, so each pattern demands the
|
|
845
|
+
// object noun rather than a bare verb wherever the verb is ordinary German.
|
|
846
|
+
//
|
|
847
|
+
// `unpublish` before nothing in particular — `\bpublish\b` does not match
|
|
848
|
+
// inside "unpublish" (no word boundary between `n` and `p`), so the two never
|
|
849
|
+
// collide.
|
|
850
|
+
// TWO SHAPES EVERY GERMAN PATTERN BELOW MUST CARRY, both learned from tests
|
|
851
|
+
// that went red rather than from inspection:
|
|
852
|
+
//
|
|
853
|
+
// 1. THE IMPERATIVE DROPS ITS `-e`. "entfern die worktree", "archivier das
|
|
854
|
+
// repo", "deaktivier den workflow" are what people type; `entfern(e|en)`
|
|
855
|
+
// matches none of them. Every verb ending is therefore `(e|en)?`.
|
|
856
|
+
// 2. WORD ORDER GOES BOTH WAYS. "archivier das repo" and "das repo
|
|
857
|
+
// archivieren" are one instruction, and a pattern anchored on the noun
|
|
858
|
+
// first sees only the second.
|
|
859
|
+
//
|
|
860
|
+
// The object noun stays REQUIRED in every pattern that has one. That is what
|
|
861
|
+
// keeps the loosened verb endings from over-authorizing: "lösch" alone
|
|
862
|
+
// authorizes nothing, "lösch den branch" authorizes a branch deletion.
|
|
863
|
+
{ op: "unpublish", re: /\bunpublish(e|en)?\b/i },
|
|
864
|
+
{
|
|
865
|
+
op: "deprecate",
|
|
866
|
+
re: /\bdeprecate(d|n)?\b|\bals\s+veraltet\s+markier(e|en)?\b/i
|
|
867
|
+
},
|
|
868
|
+
{
|
|
869
|
+
op: "release-asset",
|
|
870
|
+
re: /\brelease[-\s]?asset\b|\b(clobber|(ü|ue)berschreib(e|en)?|ersetz(e|en)?|replace|austausch(e|en)?)\b[^.\n]{0,25}\b(asset|artefakt|artifact|tarball|binary)\b|\b(asset|artefakt|artifact)\b[^.\n]{0,25}\b((ü|ue)berschreiben|ersetzen|austauschen|replace)\b/i
|
|
871
|
+
},
|
|
872
|
+
// A VERB IS REQUIRED, like `tag` and `release` above and for the same reason.
|
|
873
|
+
// A bare noun cannot tell an authorization from a mention, and here the
|
|
874
|
+
// mention is the dangerous half: "die branch-protection nicht anfassen" names
|
|
875
|
+
// the object precisely in order to forbid touching it. Measured — this was the
|
|
876
|
+
// one row of the negation corpus that neither the look-behind nor the
|
|
877
|
+
// span check could reach, because the negation trails a bare-noun match.
|
|
878
|
+
{
|
|
879
|
+
op: "protection",
|
|
880
|
+
re: /\b(entfern(e|en)?|l(ö|oe)sch(e|en)?|deaktivier(e|en)?|nimm|weg|remove|delete|disable|drop|lift)\b[^.\n]{0,30}\b(branch[-\s]?protection|protection[-\s]?rule|schutzregel|ruleset)\b|\b(branch[-\s]?protection|protection[-\s]?rule|schutzregel|ruleset)\b[^.\n]{0,30}\b(entfernen|l(ö|oe)schen|deaktivieren|abschalten|remove|delete|disable|drop|lift|weg)\b/i
|
|
881
|
+
},
|
|
882
|
+
{
|
|
883
|
+
op: "workflow-toggle",
|
|
884
|
+
re: /\bworkflow\b[^.\n]{0,25}\b(deaktivier(e|en)?|aktivier(e|en)?|disable|enable|abschalt(e|en)?|ausschalt(e|en)?)\b|\b(deaktivier(e|en)?|aktivier(e|en)?|disable|enable|abschalt(e|en)?|ausschalt(e|en)?)\b[^.\n]{0,25}\bworkflow\b/i
|
|
885
|
+
},
|
|
886
|
+
{
|
|
887
|
+
op: "repo-lifecycle",
|
|
888
|
+
re: /\b(repo|repository)\b[^.\n]{0,30}\b(archivier(e|en)?|archive|l(ö|oe)sch(e|en)?|delete)\b|\b(archivier(e|en)?|archive|l(ö|oe)sch(e|en)?|delete)\b[^.\n]{0,25}\b(repo|repository)\b|\b(repo|repository)\b[^.\n]{0,30}\bauf\s+(privat|public|öffentlich|private)\b/i
|
|
889
|
+
},
|
|
890
|
+
{
|
|
891
|
+
op: "review-approve",
|
|
892
|
+
re: /\b(approve|genehmig(e|en)?|freigeb(e|en)?|abnehm(e|en)?)\b[^.\n]{0,30}\b(review|pr|pull[-\s]request)\b|\b(review|pr|pull[-\s]request)\b[^.\n]{0,20}\b(approve|approven|genehmigen|freigeben)\b/i
|
|
893
|
+
},
|
|
894
|
+
{
|
|
895
|
+
op: "force-push",
|
|
896
|
+
re: /\bforce[-\s]?push(e|en|st)?\b|--force(-with-lease)?\b|\bmit\s+gewalt\s+push(e|en)?\b/i
|
|
897
|
+
},
|
|
898
|
+
{
|
|
899
|
+
op: "worktree-remove",
|
|
900
|
+
re: /\bworktree\b[^.\n]{0,25}\b(entfern(e|en)?|l(ö|oe)sch(e|en)?|remove|delete|weg(werfen|r(ä|ae)umen))\b|\b(entfern(e|en)?|l(ö|oe)sch(e|en)?|remove|delete)\b[^.\n]{0,25}\bworktree\b/i
|
|
901
|
+
},
|
|
902
|
+
// The `-x` flag is the whole distinction: without it `git clean` spares
|
|
903
|
+
// ignored files, with it `.env` and local certificates go too.
|
|
904
|
+
{
|
|
905
|
+
op: "clean-ignored",
|
|
906
|
+
re: /\bgit\s+clean\b[^.\n]{0,20}-[A-Za-z]*x|\b(ignorierte|ignored)\b[^.\n]{0,25}\b(dateien|files)\b[^.\n]{0,25}\b(l(ö|oe)sch(e|en)?|entfern(e|en)?|remove|delete)\b/i
|
|
907
|
+
},
|
|
908
|
+
{
|
|
909
|
+
op: "tag-force",
|
|
910
|
+
re: /\btag\b[^.\n]{0,25}\b(verschieb(e|en)?|(ü|ue)berschreib(e|en)?|umh(ä|ae)ng(e|en)?|force|move)\b|\b(verschieb(e|en)?|(ü|ue)berschreib(e|en)?)\b[^.\n]{0,20}\btag\b/i
|
|
911
|
+
},
|
|
912
|
+
{ op: "rebase", re: /\brebase(n|st|d)?\b/i },
|
|
913
|
+
{
|
|
914
|
+
op: "reset-hard",
|
|
915
|
+
re: /\breset\s+--hard\b|\bhard\s+reset\b|\bhart\s+zur(ü|ue)cksetz(e|en)?\b/i
|
|
916
|
+
},
|
|
917
|
+
{ op: "clean", re: /\bgit\s+clean\b/i },
|
|
918
|
+
{
|
|
919
|
+
op: "stash-drop",
|
|
920
|
+
re: /\bstash\b[^.\n]{0,25}\b(drop|clear|verwerf(e|en)?|l(ö|oe)sch(e|en)?)\b|\b(verwerf(e|en)?|l(ö|oe)sch(e|en)?|drop)\b[^.\n]{0,20}\bstash\b/i
|
|
921
|
+
},
|
|
922
|
+
{
|
|
923
|
+
op: "branch-delete",
|
|
924
|
+
re: /\b(l(ö|oe)sch(e|en)?|entfern(e|en)?|delete|remove)\b[^.\n]{0,25}\b(branch|branches|zweig)\b|\b(branch|branches|zweig)\b[^.\n]{0,25}\b(l(ö|oe)schen|entfernen|delete|remove)\b/i
|
|
925
|
+
},
|
|
926
|
+
{
|
|
927
|
+
op: "close",
|
|
928
|
+
re: /\b(schlie(ß|ss)(e|en)?|close)\b[^.\n]{0,30}\b(pr|pull[-\s]request|issue|ticket)\b|\b(pr|pull[-\s]request|issue|ticket)\b[^.\n]{0,20}\b(schlie(ß|ss)en|close)\b/i
|
|
929
|
+
},
|
|
930
|
+
// Enabling auto-merge is its own op on the guard side, so it needs its own
|
|
931
|
+
// sentence. Without one it is the dead end this block exists to prevent: the
|
|
932
|
+
// plain merge phrase authorizes `pr-merge` and never `pr-merge-auto`, so the
|
|
933
|
+
// refusal would have repeated forever.
|
|
934
|
+
{
|
|
935
|
+
op: "pr-merge-auto",
|
|
936
|
+
re: /\bauto[-\s]?merge\b|\bmerge\b[^.\n]{0,20}\bautomatisch\b|\bautomatisch\b[^.\n]{0,20}\bmerg(e|en)\b/i
|
|
937
|
+
}
|
|
807
938
|
];
|
|
808
939
|
var OUTPUT_LINE = /^\s*(To\s+\S+|remote:|error:|fatal:|hint:|warning:|!\s|\?\?\s|\s*\^|[-+]{3}\s|@@\s|\$\s|>\s|\d+\s+(pass|fail)|npm ERR!|Error:|Traceback|at\s+\S+:\d+)/i;
|
|
809
940
|
function isInterrogative(prose) {
|
|
@@ -864,6 +995,67 @@ function splitFences(prompt) {
|
|
|
864
995
|
function _commandLines(fence) {
|
|
865
996
|
return fence.split("\n").map((l) => l.trim()).filter((l) => l.length > 0).filter((l) => !/^(remote:|error:|hint:|fatal:|warning:|To |!\s|\s*\^)/i.test(l)).map((l) => l.replace(/^[$>#]\s*/, "")).filter((l) => /^(git|gh|npm|pnpm|yarn|task)\b/.test(l));
|
|
866
997
|
}
|
|
998
|
+
function extractMergeTargets(prompt) {
|
|
999
|
+
const out = /* @__PURE__ */ new Set();
|
|
1000
|
+
const re = /(?:#|\bpr[- ]?|\bpull[- ]request\s+#?)(\d{1,7})\b/giu;
|
|
1001
|
+
let m;
|
|
1002
|
+
while ((m = re.exec(prompt)) !== null) {
|
|
1003
|
+
const n = Number.parseInt(m[1], 10);
|
|
1004
|
+
if (Number.isSafeInteger(n) && n > 0) {
|
|
1005
|
+
out.add(n);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
return [...out].sort((a, b) => a - b);
|
|
1009
|
+
}
|
|
1010
|
+
var REVOKE_RE = /\b(stop|stopp|halt|abbrechen|abbruch|cancel|revoke|widerruf(en)?|zur(ü|ue)ck(nehmen|ziehen)|hold\s+(everything|on|the\s+merge)|warte|wait)\b|(?:\b(nicht|kein(e|en)?|no|not|dont|don't|never|niemals|nie)\b[^.!?\n]{0,30}\b(merg(e|en|ing)|zusammenf(ü|ue)hren)\b)/iu;
|
|
1011
|
+
function isRevocation(prompt) {
|
|
1012
|
+
return REVOKE_RE.test(prompt);
|
|
1013
|
+
}
|
|
1014
|
+
function _grantId(session_id, at, targets) {
|
|
1015
|
+
return `${sessionSlug(session_id) || "nosession"}:${at}:${targets.join(",")}`;
|
|
1016
|
+
}
|
|
1017
|
+
function foldGrants(prior, prompt, authorized, session_id, now) {
|
|
1018
|
+
if (isRevocation(prompt)) {
|
|
1019
|
+
return [];
|
|
1020
|
+
}
|
|
1021
|
+
const carried = prior.filter((g) => g.consumed.length < g.targets.length);
|
|
1022
|
+
if (!authorized.includes("pr-merge")) {
|
|
1023
|
+
return carried;
|
|
1024
|
+
}
|
|
1025
|
+
const targets = extractMergeTargets(prompt);
|
|
1026
|
+
if (targets.length === 0) {
|
|
1027
|
+
return carried;
|
|
1028
|
+
}
|
|
1029
|
+
const at = now.toISOString();
|
|
1030
|
+
const already = new Set(carried.flatMap((g) => g.targets));
|
|
1031
|
+
const fresh = targets.filter((t) => !already.has(t));
|
|
1032
|
+
if (fresh.length === 0) {
|
|
1033
|
+
return carried;
|
|
1034
|
+
}
|
|
1035
|
+
return [
|
|
1036
|
+
...carried,
|
|
1037
|
+
{
|
|
1038
|
+
id: _grantId(session_id, at, fresh),
|
|
1039
|
+
op: "pr-merge",
|
|
1040
|
+
targets: fresh,
|
|
1041
|
+
consumed: [],
|
|
1042
|
+
granted_at: at,
|
|
1043
|
+
evidence: prompt.trim().slice(0, 120)
|
|
1044
|
+
}
|
|
1045
|
+
];
|
|
1046
|
+
}
|
|
1047
|
+
var NEGATION_WORD = /\b(nicht|nichts|kein(e|en|em|er|es)?|niemals|nie|no|not|dont|don't|never|without|ohne)\b/i;
|
|
1048
|
+
function negatedBefore(text, index, matched = "") {
|
|
1049
|
+
let start = 0;
|
|
1050
|
+
for (const mark of [".", "!", "?", "\n"]) {
|
|
1051
|
+
const at = text.lastIndexOf(mark, index - 1);
|
|
1052
|
+
if (at + 1 > start) {
|
|
1053
|
+
start = at + 1;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
const before = text.slice(Math.max(start, index - 30), index);
|
|
1057
|
+
return NEGATION_WORD.test(before) || NEGATION_WORD.test(matched);
|
|
1058
|
+
}
|
|
867
1059
|
function classifyAuthorization(prompt) {
|
|
868
1060
|
const { prose, fences } = splitFences(prompt);
|
|
869
1061
|
const authorized = /* @__PURE__ */ new Set();
|
|
@@ -871,7 +1063,7 @@ function classifyAuthorization(prompt) {
|
|
|
871
1063
|
const instruction = prose.split("\n").filter((l) => !OUTPUT_LINE.test(l)).join("\n");
|
|
872
1064
|
for (const { op, re } of isInterrogative(instruction) ? [] : PHRASES) {
|
|
873
1065
|
const m = re.exec(instruction);
|
|
874
|
-
if (m) {
|
|
1066
|
+
if (m && !negatedBefore(instruction, m.index, m[0])) {
|
|
875
1067
|
authorized.add(op);
|
|
876
1068
|
evidence[op] = `prose: "${m[0]}"`;
|
|
877
1069
|
}
|
|
@@ -893,6 +1085,68 @@ function classifyAuthorization(prompt) {
|
|
|
893
1085
|
}
|
|
894
1086
|
return { authorized: [...authorized], evidence };
|
|
895
1087
|
}
|
|
1088
|
+
function readGrants(consumer_root, session_id) {
|
|
1089
|
+
try {
|
|
1090
|
+
const raw = fs2.readFileSync(path2.join(consumer_root, ledgerFileFor(session_id)), "utf8");
|
|
1091
|
+
const decoded = JSON.parse(raw);
|
|
1092
|
+
if (!_isObject3(decoded) || !Array.isArray(decoded["grants"])) {
|
|
1093
|
+
return [];
|
|
1094
|
+
}
|
|
1095
|
+
const owner = typeof decoded["session_id"] === "string" ? decoded["session_id"] : "";
|
|
1096
|
+
if (session_id && owner && owner !== session_id) {
|
|
1097
|
+
return [];
|
|
1098
|
+
}
|
|
1099
|
+
return decoded["grants"].flatMap((g) => {
|
|
1100
|
+
if (!_isObject3(g)) return [];
|
|
1101
|
+
const op = g["op"];
|
|
1102
|
+
if (typeof op !== "string" || !ALL_OPS.includes(op)) return [];
|
|
1103
|
+
const targets = Array.isArray(g["targets"]) ? g["targets"] : [];
|
|
1104
|
+
const consumed = Array.isArray(g["consumed"]) ? g["consumed"] : [];
|
|
1105
|
+
return [
|
|
1106
|
+
{
|
|
1107
|
+
id: String(g["id"] ?? ""),
|
|
1108
|
+
op,
|
|
1109
|
+
targets: targets.filter((t) => typeof t === "number"),
|
|
1110
|
+
consumed: consumed.filter((t) => typeof t === "number"),
|
|
1111
|
+
granted_at: String(g["granted_at"] ?? ""),
|
|
1112
|
+
evidence: String(g["evidence"] ?? "")
|
|
1113
|
+
}
|
|
1114
|
+
];
|
|
1115
|
+
});
|
|
1116
|
+
} catch {
|
|
1117
|
+
return [];
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
function consumeGrantTarget(consumer_root, session_id, target) {
|
|
1121
|
+
const file = path2.join(consumer_root, ledgerFileFor(session_id));
|
|
1122
|
+
let decoded;
|
|
1123
|
+
try {
|
|
1124
|
+
decoded = JSON.parse(fs2.readFileSync(file, "utf8"));
|
|
1125
|
+
} catch {
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
if (!_isObject3(decoded) || !Array.isArray(decoded["grants"])) {
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
let touched = false;
|
|
1132
|
+
for (const g of decoded["grants"]) {
|
|
1133
|
+
if (!_isObject3(g) || !Array.isArray(g["targets"]) || !Array.isArray(g["consumed"])) {
|
|
1134
|
+
continue;
|
|
1135
|
+
}
|
|
1136
|
+
const targets = g["targets"];
|
|
1137
|
+
const consumed = g["consumed"];
|
|
1138
|
+
if (targets.includes(target) && !consumed.includes(target)) {
|
|
1139
|
+
consumed.push(target);
|
|
1140
|
+
touched = true;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
if (touched) {
|
|
1144
|
+
try {
|
|
1145
|
+
atomic_write_json(file, decoded);
|
|
1146
|
+
} catch {
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
896
1150
|
function takePending(consumer_root, session_id, now) {
|
|
897
1151
|
const file = path2.join(consumer_root, pendingFileFor(session_id));
|
|
898
1152
|
let raw;
|
|
@@ -961,12 +1215,24 @@ function run2(stdin_text, options) {
|
|
|
961
1215
|
authorized.push(pending.op);
|
|
962
1216
|
evidence[pending.op] = `confirmation of the refused \`${pending.op}\` (refused ${pending.refused_at}): "${prompt.trim().slice(0, 40)}"`;
|
|
963
1217
|
}
|
|
1218
|
+
const now = /* @__PURE__ */ new Date();
|
|
964
1219
|
const ledger = {
|
|
965
1220
|
session_id: typeof envelope["session_id"] === "string" ? envelope["session_id"] : "",
|
|
966
|
-
detected_at:
|
|
1221
|
+
detected_at: now.toISOString(),
|
|
967
1222
|
authorized,
|
|
968
1223
|
evidence,
|
|
969
|
-
prompt_chars: prompt.length
|
|
1224
|
+
prompt_chars: prompt.length,
|
|
1225
|
+
// Grants survive this write; `authorized` does not. That asymmetry IS the
|
|
1226
|
+
// decision. A bare operation name stays one-shot exactly as `commit-policy`
|
|
1227
|
+
// requires, and only an authorization that also froze its objects earns a
|
|
1228
|
+
// life longer than the turn. See ADR-252.
|
|
1229
|
+
grants: foldGrants(
|
|
1230
|
+
readGrants(options.consumer_root, session),
|
|
1231
|
+
prompt,
|
|
1232
|
+
authorized,
|
|
1233
|
+
session,
|
|
1234
|
+
now
|
|
1235
|
+
)
|
|
970
1236
|
};
|
|
971
1237
|
try {
|
|
972
1238
|
atomic_write_json(path2.join(options.consumer_root, ledgerFileFor(session)), ledger);
|
|
@@ -1015,6 +1281,95 @@ if (_isCliEntry2()) {
|
|
|
1015
1281
|
import fs3 from "node:fs";
|
|
1016
1282
|
import path3 from "node:path";
|
|
1017
1283
|
import { fileURLToPath as fileURLToPath3, pathToFileURL as pathToFileURL3 } from "node:url";
|
|
1284
|
+
|
|
1285
|
+
// src/scripts/hooks/merge_impact.ts
|
|
1286
|
+
import { execFileSync } from "node:child_process";
|
|
1287
|
+
var DESTRUCTIVE_MARKERS = [
|
|
1288
|
+
// Schema removal. Matched on ADDED lines only — a migration that *removes* a
|
|
1289
|
+
// dropColumn call is the opposite of destructive, and matching the raw patch
|
|
1290
|
+
// would score it the same way.
|
|
1291
|
+
{ label: "migration drops a column or table", re: /^\+.*\b(dropColumn|dropTable|dropIfExists|removeColumn|drop_column|drop_table)\b/im },
|
|
1292
|
+
{ label: "raw DROP or TRUNCATE in a migration", re: /^\+.*\b(DROP\s+(TABLE|COLUMN|INDEX|CONSTRAINT)|TRUNCATE\s+TABLE)\b/im },
|
|
1293
|
+
// The author said so themselves. Conventional Commits marks a breaking change
|
|
1294
|
+
// with a `!` before the colon or a body trailer.
|
|
1295
|
+
{ label: "author marked a BREAKING CHANGE", re: /^\+.*BREAKING[ -]CHANGE/m },
|
|
1296
|
+
// A removed export is the shape most likely to break an importer.
|
|
1297
|
+
{ label: "an exported symbol was removed", re: /^-\s*export\s+(default\s+|const\s+|function\s+|class\s+|interface\s+|type\s+|async\s+)/m },
|
|
1298
|
+
{ label: "a public PHP or Python symbol was removed", re: /^-\s*(public\s+function|def\s+[a-z_]+\s*\()/m },
|
|
1299
|
+
// Route removal breaks callers that never imported anything.
|
|
1300
|
+
{ label: "a route or endpoint was removed", re: /^-.*\b(Route::(get|post|put|patch|delete)|@(Get|Post|Put|Patch|Delete)Mapping|app\.(get|post|put|patch|delete)\s*\()/m },
|
|
1301
|
+
{ label: "a whole file was deleted", re: /^deleted file mode /m }
|
|
1302
|
+
];
|
|
1303
|
+
var MAJOR_BUMP = /^-\s*"version"\s*:\s*"(\d+)\.[^"]*"[\s\S]{0,200}?^\+\s*"version"\s*:\s*"(\d+)\./m;
|
|
1304
|
+
function classifyDiff(patch) {
|
|
1305
|
+
if (patch.trim() === "") {
|
|
1306
|
+
return { verdict: "undecidable", markers: [], reason: "empty diff" };
|
|
1307
|
+
}
|
|
1308
|
+
const markers = [];
|
|
1309
|
+
for (const { label, re } of DESTRUCTIVE_MARKERS) {
|
|
1310
|
+
if (re.test(patch)) {
|
|
1311
|
+
markers.push(label);
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
const bump = MAJOR_BUMP.exec(patch);
|
|
1315
|
+
if (bump && bump[1] !== bump[2]) {
|
|
1316
|
+
markers.push(`major version bump ${bump[1]}.x to ${bump[2]}.x`);
|
|
1317
|
+
}
|
|
1318
|
+
const filesChanged = (patch.match(/^diff --git /gm) ?? []).length;
|
|
1319
|
+
if (markers.length > 0) {
|
|
1320
|
+
return { verdict: "destructive", markers, filesChanged };
|
|
1321
|
+
}
|
|
1322
|
+
return { verdict: "additive", markers: [], filesChanged };
|
|
1323
|
+
}
|
|
1324
|
+
function fetchPatch(pr, opts = {}) {
|
|
1325
|
+
const run5 = opts.exec ?? execFileSync;
|
|
1326
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
1327
|
+
const capture = (file, args) => run5(file, args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 15e3 });
|
|
1328
|
+
for (const ref of [`refs/pull/${pr}/head`, `pull/${pr}/head`]) {
|
|
1329
|
+
try {
|
|
1330
|
+
const base = capture("git", ["merge-base", "HEAD", ref]).trim();
|
|
1331
|
+
if (base) {
|
|
1332
|
+
const patch = capture("git", ["diff", `${base}...${ref}`]);
|
|
1333
|
+
if (patch.trim() !== "") {
|
|
1334
|
+
return { patch, source: "local" };
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
} catch {
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
if (opts.allowNetwork === false) {
|
|
1341
|
+
return { patch: "", source: "none", reason: "no local ref and network reads disabled" };
|
|
1342
|
+
}
|
|
1343
|
+
try {
|
|
1344
|
+
const patch = capture("gh", ["pr", "diff", String(pr), "--patch"]);
|
|
1345
|
+
if (patch.trim() !== "") {
|
|
1346
|
+
return { patch, source: "gh" };
|
|
1347
|
+
}
|
|
1348
|
+
return { patch: "", source: "none", reason: "gh returned an empty diff" };
|
|
1349
|
+
} catch {
|
|
1350
|
+
return { patch: "", source: "none", reason: "no local ref, and gh pr diff failed" };
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
function analyseMergeImpact(pr, opts = {}) {
|
|
1354
|
+
const got = fetchPatch(pr, opts);
|
|
1355
|
+
if (got.source === "none") {
|
|
1356
|
+
return { verdict: "undecidable", markers: [], ...got.reason ? { reason: got.reason } : {} };
|
|
1357
|
+
}
|
|
1358
|
+
return classifyDiff(got.patch);
|
|
1359
|
+
}
|
|
1360
|
+
function describeImpact(pr, impact) {
|
|
1361
|
+
const say = `Say "merge #${pr}" to authorize it.`;
|
|
1362
|
+
switch (impact.verdict) {
|
|
1363
|
+
case "additive":
|
|
1364
|
+
return `Impact scan: PR #${pr} touches ${impact.filesChanged ?? 0} file(s) and trips no destructive marker \u2014 no schema drop, no removed export or route, no major bump, no BREAKING CHANGE. Additive as far as the diff can show, which is not the same as safe to merge: the base has moved since review and merging may trigger a deploy. ${say}`;
|
|
1365
|
+
case "destructive":
|
|
1366
|
+
return `Impact scan: PR #${pr} is NOT additive \u2014 ${impact.markers.join("; ")}. Read the diff before authorizing. ${say}`;
|
|
1367
|
+
default:
|
|
1368
|
+
return `Impact scan: could not read the diff for PR #${pr} (${impact.reason ?? "unknown"}), so nothing is known about what it changes. ${say}`;
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
// src/scripts/hooks/block_unauthorized_git.ts
|
|
1018
1373
|
var EXIT_ALLOW3 = 0;
|
|
1019
1374
|
var EXIT_BLOCK = 1;
|
|
1020
1375
|
function _isObject4(v) {
|
|
@@ -1027,13 +1382,52 @@ var BLOCK_OPS = /* @__PURE__ */ new Set([
|
|
|
1027
1382
|
"pr-merge",
|
|
1028
1383
|
// Enabling auto-merge commits the outcome to a condition the agent does not
|
|
1029
1384
|
// control, so it is blocked on the same terms as the merge it schedules.
|
|
1030
|
-
"pr-merge-auto"
|
|
1385
|
+
"pr-merge-auto",
|
|
1386
|
+
// ── Added 2026-09-03. Every one is already never-autonomous in the
|
|
1387
|
+
// `non-destructive-by-default` prose; the measurement that drove it is in the
|
|
1388
|
+
// `GitOp` union's own note — 17 of 25 borderline operations classified as
|
|
1389
|
+
// NOTHING, including three worse than anything the guard already caught.
|
|
1390
|
+
/** The undo that is more destructive than the act: lockfiles break. */
|
|
1391
|
+
"unpublish",
|
|
1392
|
+
/** Metadata only, and every install on earth starts warning. */
|
|
1393
|
+
"deprecate",
|
|
1394
|
+
/** Replaces bytes consumers already hold a checksum for. */
|
|
1395
|
+
"release-asset",
|
|
1396
|
+
/** Deletes the guard rather than the data — everything after is unguarded. */
|
|
1397
|
+
"protection",
|
|
1398
|
+
/** No data lost; the gate that protects everything downstream is off. */
|
|
1399
|
+
"workflow-toggle",
|
|
1400
|
+
/** Reach outside the repository, and mostly one-way. */
|
|
1401
|
+
"repo-lifecycle",
|
|
1402
|
+
/** No code moves, and an armed auto-merge is released. */
|
|
1403
|
+
"review-approve",
|
|
1404
|
+
/** Discards commits that landed after the last fetch, on a shared ref. */
|
|
1405
|
+
"force-push",
|
|
1406
|
+
/** Destroys uncommitted work belonging to a session this one cannot see. */
|
|
1407
|
+
"worktree-remove",
|
|
1408
|
+
/** `-x` is the flag that takes `.env` and local certificates. */
|
|
1409
|
+
"clean-ignored"
|
|
1031
1410
|
]);
|
|
1032
1411
|
var WARN_OPS = /* @__PURE__ */ new Set([
|
|
1033
1412
|
"commit",
|
|
1034
1413
|
"push",
|
|
1035
1414
|
"pr-create",
|
|
1036
|
-
"branch"
|
|
1415
|
+
"branch",
|
|
1416
|
+
// ── Added 2026-09-03, deliberately at warn rather than block.
|
|
1417
|
+
//
|
|
1418
|
+
// Each is destructive in some reading, and each is also routine enough that
|
|
1419
|
+
// blocking it would be the friction that gets the whole guard routed around —
|
|
1420
|
+
// the failure mode this estate has already recorded twice. `reset-hard` is the
|
|
1421
|
+
// sharpest case and stays here for a decidable reason: the Hard Floor's
|
|
1422
|
+
// qualifier is "past unpushed work", and the command text cannot say whether
|
|
1423
|
+
// the work was pushed.
|
|
1424
|
+
"tag-force",
|
|
1425
|
+
"rebase",
|
|
1426
|
+
"reset-hard",
|
|
1427
|
+
"clean",
|
|
1428
|
+
"stash-drop",
|
|
1429
|
+
"branch-delete",
|
|
1430
|
+
"close"
|
|
1037
1431
|
]);
|
|
1038
1432
|
var G = "(?:-[A-Za-z-]+(?:=\\S+)?(?:\\s+\\S+)?\\s+)*";
|
|
1039
1433
|
var P = "^(?:\\S*\\/)?";
|
|
@@ -1079,6 +1473,72 @@ var COMMAND_OPS = [
|
|
|
1079
1473
|
op: "pr-create",
|
|
1080
1474
|
re: new RegExp(`${P}gh\\s+pr\\s+create\\b|${ghApiWrite("\\/pulls\\b").source}`, "i")
|
|
1081
1475
|
},
|
|
1476
|
+
// ── Added 2026-09-03. Placement inside this table is as load-bearing as the
|
|
1477
|
+
// patterns: the loop takes the MOST SEVERE match per segment, but within one
|
|
1478
|
+
// op the first pattern wins, so anything more specific than a plain `git push`
|
|
1479
|
+
// has to sit above it.
|
|
1480
|
+
// `npm unpublish` is NOT reached by the publish pattern — `\bpublish\b` needs
|
|
1481
|
+
// a word boundary that `unpublish` does not provide between `n` and `p`. That
|
|
1482
|
+
// is why it classified as nothing for as long as it did.
|
|
1483
|
+
{ op: "unpublish", re: new RegExp(`${P}(npm|pnpm|yarn)\\s+${G}unpublish\\b`, "i") },
|
|
1484
|
+
{ op: "deprecate", re: new RegExp(`${P}(npm|pnpm|yarn)\\s+${G}deprecate\\b`, "i") },
|
|
1485
|
+
{
|
|
1486
|
+
op: "release-asset",
|
|
1487
|
+
re: new RegExp(`${P}gh\\s+release\\s+(upload|edit|delete)\\b`, "i")
|
|
1488
|
+
},
|
|
1489
|
+
{
|
|
1490
|
+
op: "protection",
|
|
1491
|
+
re: new RegExp(
|
|
1492
|
+
`${P}gh\\s+api\\b(?=[^\\n]*(?:protection|rulesets?)\\b)(?=[^\\n]*(?:-X|--method)\\s+(?:DELETE|PUT|PATCH|POST)\\b)|${P}gh\\s+ruleset\\s+delete\\b`,
|
|
1493
|
+
"i"
|
|
1494
|
+
)
|
|
1495
|
+
},
|
|
1496
|
+
{
|
|
1497
|
+
op: "workflow-toggle",
|
|
1498
|
+
re: new RegExp(`${P}gh\\s+workflow\\s+(disable|enable)\\b`, "i")
|
|
1499
|
+
},
|
|
1500
|
+
{
|
|
1501
|
+
op: "repo-lifecycle",
|
|
1502
|
+
re: new RegExp(
|
|
1503
|
+
`${P}gh\\s+repo\\s+(archive|delete|unarchive)\\b|${P}gh\\s+repo\\s+edit\\b(?=[^\\n]*--visibility\\b)`,
|
|
1504
|
+
"i"
|
|
1505
|
+
)
|
|
1506
|
+
},
|
|
1507
|
+
{
|
|
1508
|
+
op: "review-approve",
|
|
1509
|
+
re: new RegExp(`${P}gh\\s+pr\\s+review\\b(?=[^\\n]*--approve\\b)`, "i")
|
|
1510
|
+
},
|
|
1511
|
+
// Above `push`, and both spellings: `--force` and `--force-with-lease`. The
|
|
1512
|
+
// "safe force" is in the same class here because what it is safe against is a
|
|
1513
|
+
// stale local view, not a collaborator who pushed after your fetch.
|
|
1514
|
+
{
|
|
1515
|
+
op: "force-push",
|
|
1516
|
+
re: new RegExp(`${P}git\\s+${G}push\\b[^\\n;|&]*(--force\\b|--force-with-lease\\b|\\s-f\\b)`, "i")
|
|
1517
|
+
},
|
|
1518
|
+
{
|
|
1519
|
+
op: "worktree-remove",
|
|
1520
|
+
re: new RegExp(`${P}git\\s+worktree\\s+remove\\b[^\\n;|&]*(--force\\b|\\s-f\\b)`, "i")
|
|
1521
|
+
},
|
|
1522
|
+
// `-x` (or `-X`) anywhere in the flag cluster is what reaches ignored files.
|
|
1523
|
+
{
|
|
1524
|
+
op: "clean-ignored",
|
|
1525
|
+
re: new RegExp(`${P}git\\s+clean\\b[^\\n;|&]*-[A-Za-z]*x`, "i")
|
|
1526
|
+
},
|
|
1527
|
+
{ op: "tag-force", re: new RegExp(`${P}git\\s+tag\\b[^\\n;|&]*(-f\\b|--force\\b)`, "i") },
|
|
1528
|
+
{ op: "rebase", re: new RegExp(`${P}git\\s+${G}rebase\\b`, "i") },
|
|
1529
|
+
{ op: "reset-hard", re: new RegExp(`${P}git\\s+${G}reset\\b[^\\n;|&]*--hard\\b`, "i") },
|
|
1530
|
+
{ op: "clean", re: new RegExp(`${P}git\\s+clean\\b`, "i") },
|
|
1531
|
+
{ op: "stash-drop", re: new RegExp(`${P}git\\s+stash\\s+(drop|clear)\\b`, "i") },
|
|
1532
|
+
// Remote deletion and the local `-D` are one op: both remove a ref, and the
|
|
1533
|
+
// remote form additionally closes any open PR pointing at it.
|
|
1534
|
+
{
|
|
1535
|
+
op: "branch-delete",
|
|
1536
|
+
re: new RegExp(
|
|
1537
|
+
`${P}git\\s+branch\\b[^\\n;|&]*(-D\\b|--delete\\b)|${P}git\\s+${G}push\\b[^\\n;|&]*(--delete\\b|\\s:\\S)`,
|
|
1538
|
+
"i"
|
|
1539
|
+
)
|
|
1540
|
+
},
|
|
1541
|
+
{ op: "close", re: new RegExp(`${P}gh\\s+(pr|issue)\\s+close\\b`, "i") },
|
|
1082
1542
|
{ op: "push", re: new RegExp(`${P}git\\s+${G}push\\b`, "i") },
|
|
1083
1543
|
{ op: "commit", re: new RegExp(`${P}git\\s+${G}commit\\b`, "i") },
|
|
1084
1544
|
{ op: "branch", re: new RegExp(`${P}git\\s+${G}(checkout\\s+-b|switch\\s+-c)\\b`, "i") }
|
|
@@ -1271,8 +1731,25 @@ function commandOp(command) {
|
|
|
1271
1731
|
}
|
|
1272
1732
|
return found;
|
|
1273
1733
|
}
|
|
1734
|
+
function mergeTargetOf(command) {
|
|
1735
|
+
const gh = /\bgh\s+pr\s+merge\s+(?:-{1,2}\S+\s+)*?(\d{1,7})\b/i.exec(command);
|
|
1736
|
+
if (gh) {
|
|
1737
|
+
return Number.parseInt(gh[1], 10);
|
|
1738
|
+
}
|
|
1739
|
+
const api = /\/pulls\/(\d{1,7})\/merge\b/i.exec(command);
|
|
1740
|
+
return api ? Number.parseInt(api[1], 10) : null;
|
|
1741
|
+
}
|
|
1742
|
+
function grantCovers(grants, op, target) {
|
|
1743
|
+
if (op !== "pr-merge" || target === null) {
|
|
1744
|
+
return null;
|
|
1745
|
+
}
|
|
1746
|
+
return grants.find(
|
|
1747
|
+
(g) => g.op === "pr-merge" && g.targets.includes(target) && !g.consumed.includes(target)
|
|
1748
|
+
) ?? null;
|
|
1749
|
+
}
|
|
1274
1750
|
var LEDGER_MAX_AGE_MS = 30 * 60 * 1e3;
|
|
1275
1751
|
function _loadLedger(consumer_root, session_id, now) {
|
|
1752
|
+
const grants = readGrants(consumer_root, session_id);
|
|
1276
1753
|
const candidates = [ledgerFileFor(session_id)];
|
|
1277
1754
|
if (!candidates.includes(STATE_FILE2)) {
|
|
1278
1755
|
candidates.push(STATE_FILE2);
|
|
@@ -1280,10 +1757,10 @@ function _loadLedger(consumer_root, session_id, now) {
|
|
|
1280
1757
|
for (const rel of candidates) {
|
|
1281
1758
|
const found = _readLedgerFile(path3.join(consumer_root, rel), session_id, now);
|
|
1282
1759
|
if (found !== null) {
|
|
1283
|
-
return found;
|
|
1760
|
+
return { ...found, grants };
|
|
1284
1761
|
}
|
|
1285
1762
|
}
|
|
1286
|
-
return { authorized: /* @__PURE__ */ new Set(), present: false };
|
|
1763
|
+
return { authorized: /* @__PURE__ */ new Set(), present: false, grants };
|
|
1287
1764
|
}
|
|
1288
1765
|
function _readLedgerFile(file, session_id, now) {
|
|
1289
1766
|
try {
|
|
@@ -1307,7 +1784,7 @@ function _readLedgerFile(file, session_id, now) {
|
|
|
1307
1784
|
}
|
|
1308
1785
|
return null;
|
|
1309
1786
|
}
|
|
1310
|
-
function decide(command, ledger) {
|
|
1787
|
+
function decide(command, ledger, impact) {
|
|
1311
1788
|
if (command === null) {
|
|
1312
1789
|
return { exit: EXIT_ALLOW3, stdout: "", stderr: "" };
|
|
1313
1790
|
}
|
|
@@ -1315,9 +1792,14 @@ function decide(command, ledger) {
|
|
|
1315
1792
|
if (op === null || ledger.authorized.has(op)) {
|
|
1316
1793
|
return { exit: EXIT_ALLOW3, stdout: "", stderr: "" };
|
|
1317
1794
|
}
|
|
1795
|
+
const target = mergeTargetOf(command);
|
|
1796
|
+
if (grantCovers(ledger.grants ?? [], op, target) && target !== null) {
|
|
1797
|
+
return { exit: EXIT_ALLOW3, stdout: "", stderr: "", consumedTarget: target };
|
|
1798
|
+
}
|
|
1318
1799
|
const shown = command.length > 120 ? `${command.slice(0, 117)}\u2026` : command;
|
|
1319
1800
|
if (BLOCK_OPS.has(op)) {
|
|
1320
|
-
const
|
|
1801
|
+
const scan = op === "pr-merge" && target !== null && impact ? ` ${impact}` : "";
|
|
1802
|
+
const reason = `Blocked: \`${op}\` with no authorization in this turn's prompt. This operation is irreversible and non-destructive-by-default already declares it never-autonomous. Command: ${shown}.${scan} Ask the user, in one numbered-options block, and run it only after they answer this turn.`;
|
|
1321
1803
|
return { exit: EXIT_BLOCK, stdout: "", stderr: `${reason}
|
|
1322
1804
|
`, blockedOp: op };
|
|
1323
1805
|
}
|
|
@@ -1345,10 +1827,24 @@ function run3(stdin_text, options) {
|
|
|
1345
1827
|
}
|
|
1346
1828
|
}
|
|
1347
1829
|
const session_id = typeof envelope["session_id"] === "string" ? envelope["session_id"] : "";
|
|
1348
|
-
const
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
);
|
|
1830
|
+
const command = extractCommand(envelope);
|
|
1831
|
+
const ledger = _loadLedger(options.consumer_root, session_id, Date.now());
|
|
1832
|
+
let impact;
|
|
1833
|
+
const stage1 = command === null ? null : commandOp(command);
|
|
1834
|
+
const stage2Target = command === null ? null : mergeTargetOf(command);
|
|
1835
|
+
if (stage1 === "pr-merge" && stage2Target !== null && !ledger.authorized.has("pr-merge") && !grantCovers(ledger.grants ?? [], "pr-merge", stage2Target)) {
|
|
1836
|
+
try {
|
|
1837
|
+
impact = describeImpact(
|
|
1838
|
+
stage2Target,
|
|
1839
|
+
analyseMergeImpact(stage2Target, { cwd: options.consumer_root })
|
|
1840
|
+
);
|
|
1841
|
+
} catch {
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
const decision = decide(command, ledger, impact);
|
|
1845
|
+
if (decision.consumedTarget !== void 0) {
|
|
1846
|
+
consumeGrantTarget(options.consumer_root, session_id, decision.consumedTarget);
|
|
1847
|
+
}
|
|
1352
1848
|
if (decision.blockedOp) {
|
|
1353
1849
|
try {
|
|
1354
1850
|
atomic_write_json(path3.join(options.consumer_root, pendingFileFor(session_id)), {
|