@event4u/agent-config 14.18.0 → 14.19.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 +169 -130
- package/README.md +71 -149
- package/dist/agent-src/README.md +1 -1
- package/dist/agent-src/commands/agent-handoff.md +2 -2
- package/dist/agent-src/commands/review/changes.md +19 -19
- package/dist/agent-src/commands/review.md +2 -2
- package/dist/agent-src/contexts/execution/host-capability-manifest.md +30 -3
- package/dist/agent-src/rules/autonomous-execution.md +1 -1
- package/dist/agent-src/rules/git-history-discipline.md +2 -2
- package/dist/agent-src/rules/guidelines.md +1 -1
- package/dist/agent-src/rules/session-canary.md +2 -2
- package/dist/agent-src/rules/token-efficiency.md +1 -1
- package/dist/agent-src/skills/adversarial-review/SKILL.md +0 -8
- package/dist/agent-src/skills/canvas-design/SKILL.md +1 -1
- package/dist/agent-src/skills/design-intelligence/SKILL.md +1 -1
- package/dist/agent-src/skills/design-intelligence/data/app-interface.csv +1 -1
- package/dist/agent-src/skills/design-intelligence/data/motion.csv +17 -17
- package/dist/agent-src/skills/design-intelligence/data/ux-guidelines.csv +2 -2
- package/dist/agent-src/skills/design-intelligence/references/context-and-registers.md +17 -0
- package/dist/agent-src/skills/design-intelligence/references/design-rules-checklist.md +4 -4
- package/dist/agent-src/skills/design-review/SKILL.md +1 -1
- package/dist/agent-src/skills/design-variations/SKILL.md +1 -1
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +1 -1
- package/dist/agent-src/skills/fe-design/SKILL.md +32 -5
- package/dist/agent-src/skills/fe-design/references/craft-floor.md +7 -0
- package/dist/agent-src/skills/fe-design/references/design-patterns.md +47 -2
- package/dist/agent-src/skills/laravel-scheduling/SKILL.md +3 -1
- package/dist/agent-src/skills/project-analyzer/SKILL.md +0 -6
- package/dist/agent-src/skills/react-native-setup/SKILL.md +3 -3
- package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +1 -1
- package/dist/agent-src/skills/sequential-thinking/SKILL.md +18 -8
- package/dist/agent-src/skills/tailwind-engineer/SKILL.md +1 -1
- package/dist/agent-src/skills/ui-apply-generic/SKILL.md +1 -1
- package/dist/agent-src/skills/ui-component-architect/SKILL.md +1 -1
- package/dist/agent-src/skills/using-git-worktrees/SKILL.md +17 -2
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli/commands/recordTriggerEval.js +2 -2
- package/dist/cli/main.js +1 -1
- package/dist/cli-delegate/{chunk-AKJGUQNO.js → chunk-GZTMRYCN.js} +405 -341
- package/dist/cli-delegate/{chunk-7RXIKF62.js → chunk-IDHUQIXP.js} +283 -132
- package/dist/cli-delegate/cmd_adr_effective.js +318 -81
- package/dist/cli-delegate/cmd_conformance.js +420 -16
- package/dist/cli-delegate/cmd_doctor.js +1 -1
- package/dist/cli-delegate/cmd_export.js +1 -1
- package/dist/cli-delegate/cmd_handoff.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +1 -1
- package/dist/cli-delegate/cmd_route_audit.js +1 -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 +28 -27
- 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 +2132 -1462
- package/dist/install/install.mjs +588 -527
- package/dist/mcp/registry-manifest.json +1 -1
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1619 -1045
- package/dist/scripts/_lib/model_tier.js +1 -1
- package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +1 -1
- package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +1 -1
- package/docs/guidelines/design-antipatterns.md +87 -64
- package/package.json +1 -1
- package/src/agent-src/README.md +1 -1
- package/src/config/ci-local-parity.yml +6 -0
- package/src/config/gate-coverage.yml +170 -0
- package/src/config/gate-violation-baselines.json +3 -3
- package/src/config/release-gate-locality.yml +12 -0
- package/src/scripts/_cli/cmd_doctor.ts +2 -2
- package/src/scripts/_cli/cmd_handoff.ts +1 -1
- package/src/scripts/_cli/cmd_update.ts +1 -1
- package/src/scripts/_cli/cmd_versions.ts +1 -1
- package/src/scripts/_lib/adr_frontmatter.ts +44 -0
- package/src/scripts/_lib/consequence_objects.ts +152 -0
- package/src/scripts/_lib/exec_evidence.ts +7 -0
- package/src/scripts/_lib/global_deploy_inventory.ts +2 -2
- package/src/scripts/_lib/host_permission_checks.ts +251 -0
- package/src/scripts/_lib/model_tier.ts +1 -1
- package/src/scripts/_lib/obligation_frequency.ts +2 -2
- package/src/scripts/_lib/release_highlights.ts +93 -0
- package/src/scripts/_lib/release_material.ts +64 -1
- package/src/scripts/_lib/runtime_journal.ts +26 -3
- package/src/scripts/_lib/runtime_wiring_checks.ts +12 -1
- package/src/scripts/_lib/session_register.ts +11 -6
- package/src/scripts/_lib/sqlite_schema.ts +22 -0
- package/src/scripts/_lib/surface.ts +86 -0
- package/src/scripts/_lib/ui_authority.ts +116 -0
- package/src/scripts/adr/regenerate_index.ts +54 -1
- package/src/scripts/adr_cite_check.ts +71 -15
- package/src/scripts/ai_council/clients.ts +1 -1
- package/src/scripts/autonomy_friction_corpus.ts +229 -0
- package/src/scripts/build_proof.ts +15 -2
- package/src/scripts/check_beta_review_markers.ts +141 -17
- package/src/scripts/check_enforcement_coverage.ts +1 -1
- package/src/scripts/check_enforcement_denominator.ts +121 -11
- package/src/scripts/check_finding_dispositions.ts +66 -9
- package/src/scripts/check_gate_coverage.ts +96 -0
- package/src/scripts/check_release_highlights.ts +74 -1
- package/src/scripts/check_skill_activation_claim.ts +179 -0
- package/src/scripts/check_trigger_evals.ts +1 -1
- package/src/scripts/conformance_scan.ts +44 -3
- package/src/scripts/explain_run.ts +1 -1
- package/src/scripts/generate_capability_matrix.ts +1 -1
- package/src/scripts/git_authorization_hook.ts +75 -1
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +14 -11
- package/src/scripts/hooks/augment-dispatcher.sh +1 -1
- package/src/scripts/hooks/category_a.ts +291 -0
- package/src/scripts/hooks/cline-dispatcher.sh +1 -1
- package/src/scripts/hooks/cursor-dispatcher.sh +11 -5
- package/src/scripts/hooks/dispatch_hook.ts +17 -22
- package/src/scripts/hooks/git_command_classifier.ts +156 -1
- package/src/scripts/hooks/host_lowering.ts +215 -0
- package/src/scripts/hooks/host_lowering.yaml +185 -0
- package/src/scripts/hooks/host_semantics.ts +105 -37
- package/src/scripts/hooks/journal_record_hook.ts +10 -0
- package/src/scripts/hooks/permission_gate.ts +117 -0
- package/src/scripts/hooks_status.ts +12 -10
- package/src/scripts/install.ts +53 -82
- package/src/scripts/install_anthropic_key.sh +2 -2
- package/src/scripts/lint_design_antipattern_parity.ts +80 -0
- package/src/scripts/lint_evidence_artifacts.ts +45 -1
- package/src/scripts/lint_hook_manifest.ts +87 -3
- package/src/scripts/lint_motion_authority_drift.ts +731 -0
- package/src/scripts/lint_readme_size.ts +158 -12
- package/src/scripts/lint_reasoning_fixtures.ts +216 -0
- package/src/scripts/lint_skill_trigger_contradiction.ts +161 -0
- package/src/scripts/measure_sibling_naming.ts +150 -0
- package/src/scripts/redact_hook_capture.ts +1 -1
- package/src/scripts/report_skill_activation.ts +319 -7
- package/src/scripts/schemas/review-findings.schema.json +5 -0
- package/src/scripts/schemas/skill.schema.json +2 -2
- package/src/scripts/session_canary_hook.ts +1 -1
- package/src/scripts/session_register_hook.ts +4 -3
- package/src/scripts/trigger_eval_grandfather.json +5 -1
- package/src/scripts/validate_agent_settings.ts +1 -1
|
@@ -483,3 +483,47 @@ export function readAdrAxisCells(text: string): AdrAxisCells {
|
|
|
483
483
|
: `${strength} (${discovery})`,
|
|
484
484
|
};
|
|
485
485
|
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* The one shape this tool decides: a trigger whose condition IS a leading date.
|
|
489
|
+
*
|
|
490
|
+
* Deliberately anchored at the start of the string and deliberately narrow. The
|
|
491
|
+
* boundary is the whole safety property — the moment a trigger is matched on a
|
|
492
|
+
* date found anywhere in its prose, "reopens when N exceeds M" is one edit away
|
|
493
|
+
* and the machine-readable grammar two council seats rejected on 2026-08-19 has
|
|
494
|
+
* been built by increments.
|
|
495
|
+
*/
|
|
496
|
+
export const DATED_TRIGGER_RE = /^\s*(?:expir(?:y|es)\b\s*[:—-]?\s*)?(\d{4})-(\d{2})-(\d{2})(?!\d)/iu;
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* A trigger that ANNOUNCES itself as dated, whether or not the date parses.
|
|
500
|
+
*
|
|
501
|
+
* The gap between this and `DATED_TRIGGER_RE` is the `dated-unparsed` state.
|
|
502
|
+
* Exactly one ADR in the corpus carries a dated expiry, so a parse tuned to its
|
|
503
|
+
* phrasing can fail on the second one — and the failure mode is silence,
|
|
504
|
+
* because the old answer for every trigger was `indeterminate`. This probe is
|
|
505
|
+
* what turns that silence into a reported defect.
|
|
506
|
+
*/
|
|
507
|
+
export const DATED_TRIGGER_PROBE = /^\s*(?:expir(?:y|es)\b|\d{4}-\d{1,2}-\d{1,2}\b)/iu;
|
|
508
|
+
|
|
509
|
+
/** The UTC calendar day of an instant, as `YYYY-MM-DD`. */
|
|
510
|
+
export function iso_day(d: Date): string {
|
|
511
|
+
return d.toISOString().slice(0, 10);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* The calendar day a dated trigger names, or null when there is none to read.
|
|
516
|
+
*
|
|
517
|
+
* Round-tripping through `Date.UTC` rejects `2026-13-45`, which matches the
|
|
518
|
+
* shape and is not a date.
|
|
519
|
+
*/
|
|
520
|
+
export function dated_trigger_day(trigger: string): string | null {
|
|
521
|
+
const m = DATED_TRIGGER_RE.exec(trigger);
|
|
522
|
+
if (m === null) return null;
|
|
523
|
+
const [y, mo, d] = [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
524
|
+
const at = new Date(Date.UTC(y, mo - 1, d));
|
|
525
|
+
if (at.getUTCFullYear() !== y || at.getUTCMonth() !== mo - 1 || at.getUTCDate() !== d) {
|
|
526
|
+
return null;
|
|
527
|
+
}
|
|
528
|
+
return iso_day(at);
|
|
529
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* consequence_objects — the consequence vocabulary's prose half.
|
|
3
|
+
*
|
|
4
|
+
* Two things live here: the phrase rows that recognise a consequence
|
|
5
|
+
* authorization, and the extraction of the OBJECT such an authorization names.
|
|
6
|
+
* An authorization for one table, one environment or one recipient class is not
|
|
7
|
+
* an authorization for another, and until this existed the ledger could express
|
|
8
|
+
* the difference for nothing but pull-request numbers.
|
|
9
|
+
*
|
|
10
|
+
* IT IS RECOGNITION AND EXTRACTION, NOT MATCHING. Nothing here decides
|
|
11
|
+
* anything; the one consumer that acts on the vocabulary is `conformance_scan`,
|
|
12
|
+
* which counts after the fact. Making the object a matching condition is the
|
|
13
|
+
* ledger item ADR-254 left to the owner.
|
|
14
|
+
*
|
|
15
|
+
* It sits beside `git_authorization_hook` rather than inside it because that
|
|
16
|
+
* file is against the per-file source-size budget, which charges every line
|
|
17
|
+
* above 1500. The split is along a real seam: the hook owns the machinery —
|
|
18
|
+
* sentence bounds, negation, fenced-paste handling — and this module owns the
|
|
19
|
+
* consequence vocabulary that machinery is applied to.
|
|
20
|
+
*/
|
|
21
|
+
import type { GitOp } from "../git_authorization_hook.js";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Prose phrases, German and English, that authorize a consequence operation.
|
|
25
|
+
*
|
|
26
|
+
* Spliced into `PHRASES` at its definition, so every row inherits the
|
|
27
|
+
* interrogative filter, the clause-scoped negation check and the fenced-paste
|
|
28
|
+
* split rather than reimplementing them.
|
|
29
|
+
*
|
|
30
|
+
* Every row REQUIRES ITS OBJECT NOUN, and that is why the patterns are long. A
|
|
31
|
+
* bare verb cannot tell an authorization from a mention, and for this class the
|
|
32
|
+
* mention is the common case: "deploy" appears in a hundred sentences that
|
|
33
|
+
* authorize nothing, while "deploy nach production" appears only when someone
|
|
34
|
+
* means it.
|
|
35
|
+
*
|
|
36
|
+
* German verb endings are `(e|en)?` because the imperative drops its `-e`, and
|
|
37
|
+
* both word orders are covered — the house style stated above `PHRASES`. The
|
|
38
|
+
* leading boundary is an explicit character class rather than `\b`: JavaScript's
|
|
39
|
+
* `\b` is ASCII-only, so a word STARTING with an umlaut ("Ändere") has no
|
|
40
|
+
* boundary before it and the row silently never fires.
|
|
41
|
+
*/
|
|
42
|
+
export const CONSEQUENCE_PHRASES: ReadonlyArray<{ op: GitOp; re: RegExp }> = [
|
|
43
|
+
{
|
|
44
|
+
op: "prod-deploy",
|
|
45
|
+
re: /(?<![A-Za-z0-9_À-ɏ])(deploy(e|en|t|st)?|ausroll(e|en)?|ship(pe|pen)?|live\s*schalt(e|en)?|rollout)\b[^.\n]{0,30}\b(produktion|production|prod|live|prod-umgebung)\b|\b(produktion|production|prod|live)\b[^.\n]{0,30}\b(deploy(e|en)?|ausrollen|ausroll(e|en)?|live\s*schalten|rollout)\b/i,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
op: "prod-data-destroy",
|
|
49
|
+
re: /(?<![A-Za-z0-9_À-ɏ])(l(ö|oe)sch(e|en)?|entfern(e|en)?|leer(e|en)?|verwerf(e|en)?|drop|truncate|delete|wipe|purge|zerst(ö|oe)r(e|en)?)\b[^.\n]{0,40}\b(tabelle|tabellen|datenbank|daten|bucket|collection|zeilen|rows|table|database|dataset)\b|\b(tabelle|tabellen|datenbank|daten|bucket|collection|table|database)\b[^.\n]{0,40}\b(l(ö|oe)schen|leeren|verwerfen|droppen|truncaten|entfernen|drop|truncate|delete|wipe|purge)\b/i,
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
op: "prod-infra-change",
|
|
53
|
+
re: /(?<![A-Za-z0-9_À-ɏ])((ver)?(ä|ae)nder(e|n)?|anpass(e|en)?|umbau(e|en)?|apply|destroy|(neu\s*)?provisionier(e|en)?)\b[^.\n]{0,40}\b(infrastruktur|infrastructure|cluster|terraform|iam|dns|netzwerk|network|firewall|policy|policies|security[-\s]group)\b|\b(infrastruktur|infrastructure|cluster|terraform|iam|dns|netzwerk|firewall)\b[^.\n]{0,40}\b((ver)?(ä|ae)ndern|anpassen|umbauen|zerst(ö|oe)ren|apply|destroy)\b/i,
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
op: "external-send",
|
|
57
|
+
re: /(?<![A-Za-z0-9_À-ɏ])(schick(e|en)?|verschick(e|en)?|send(e|en|et)?|mail(e|en)?|raussend(e|en)?|versend(e|en)?|post|publish)\b[^.\n]{0,40}\b(mail|mails|email|e-mail|nachricht|nachrichten|newsletter|webhook|kunden|kundin|kunde|empf(ä|ae)nger|customers?|recipients?|message)\b|\b(mail|email|e-mail|nachricht|newsletter|kunden|customers?)\b[^.\n]{0,40}\b(schicken|verschicken|senden|versenden|rausschicken|send|deliver)\b/i,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
op: "money-movement",
|
|
61
|
+
re: /(?<![A-Za-z0-9_À-ɏ])(erstatt(e|en)?|r(ü|ue)ckerstatt(e|en)?|zahl(e|en)?|(ü|ue)berweis(e|en)?|abbuch(e|en)?|belast(e|en)?|refund|charge|payout|transfer|bill)\b[^.\n]{0,40}\b(betrag|geld|rechnung|zahlung|euro|dollar|kunde|kunden|karte|konto|invoice|payment|customer|card|account)\b|\b(betrag|geld|rechnung|zahlung|invoice|payment)\b[^.\n]{0,40}\b(erstatten|zur(ü|ue)ckzahlen|(ü|ue)berweisen|abbuchen|belasten|refund|charge|transfer)\b/i,
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
op: "trunk-force-push",
|
|
65
|
+
re: /(?<![A-Za-z0-9_À-ɏ])(force[-\s]?push(e|en|t)?|force[-\s]?pushen|(ü|ue)berschreib(e|en)?)\b[^.\n]{0,40}\b(main|master|trunk|produktions?[-\s]?branch|production|release[-\s]?branch)\b|\b(main|master|trunk|production)\b[^.\n]{0,40}\b(force[-\s]?pushen|force[-\s]?push|(ü|ue)berschreiben)\b/i,
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
op: "out-of-scope-destruction",
|
|
69
|
+
re: /(?<![A-Za-z0-9_À-ɏ])(l(ö|oe)sch(e|en)?|entfern(e|en)?|r(ä|ae)um(e|en)?\s*auf|aufr(ä|ae)um(e|en)?|delete|remove|wipe)\b[^.\n]{0,40}\b(au(ß|ss)erhalb|ausserhalb|unabh(ä|ae)ngig|nicht\s+zur\s+aufgabe|outside\s+(the\s+)?(task|scope)|unrelated|everything\s+else|alles\s+andere)\b|\b(au(ß|ss)erhalb|outside\s+(the\s+)?(task|scope)|unrelated)\b[^.\n]{0,40}\b(l(ö|oe)schen|entfernen|aufr(ä|ae)umen|delete|remove|wipe)\b/i,
|
|
70
|
+
},
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Operations whose authorization is meaningless without its object.
|
|
75
|
+
*
|
|
76
|
+
* "Delete the table" is not an authorization to delete a table; it is an
|
|
77
|
+
* authorization to delete THAT table. The same holds for a recipient class and
|
|
78
|
+
* for a payment. The other four consequence operations are environment-scoped
|
|
79
|
+
* rather than object-scoped, so an object is recorded for them when the prose
|
|
80
|
+
* names one and never required.
|
|
81
|
+
*/
|
|
82
|
+
export const OBJECT_REQUIRED_OPS: ReadonlySet<GitOp> = new Set<GitOp>([
|
|
83
|
+
"prod-data-destroy",
|
|
84
|
+
"external-send",
|
|
85
|
+
"money-movement",
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Words that can follow an object noun and are never the object's NAME.
|
|
90
|
+
*
|
|
91
|
+
* "Lösch die Tabelle bitte" names no table; the extractor's next-token rule
|
|
92
|
+
* captures `bitte` unless something says otherwise. A stop list is the honest
|
|
93
|
+
* shape for that — the same reviewable-keyword-list discipline the negation and
|
|
94
|
+
* pasted-output vocabularies above use, and for the same reason: a parser that
|
|
95
|
+
* guessed here would record a fabricated object, and a fabricated object is
|
|
96
|
+
* worse than a missing one because the conformance count treats a recorded
|
|
97
|
+
* object as an authorization the turn actually gave.
|
|
98
|
+
*/
|
|
99
|
+
const OBJECT_STOP_WORDS: ReadonlySet<string> = new Set([
|
|
100
|
+
"bitte", "doch", "mal", "nicht", "jetzt", "gleich", "auch", "noch", "wieder",
|
|
101
|
+
"in", "im", "aus", "von", "vom", ": ", "der", "die", "das", "den", "dem",
|
|
102
|
+
"und", "oder", "mit", "auf", "zu", "komplett", "ganz", "alle", "allen",
|
|
103
|
+
"please", "now", "again", "too", "also", "from", "and", "or", "the", "a",
|
|
104
|
+
"an", "there", "here", "completely", "all", "with", "on", "to", "in",
|
|
105
|
+
]);
|
|
106
|
+
|
|
107
|
+
/** Lower-case, strip surrounding quotes and trailing punctuation. */
|
|
108
|
+
function _normalizeObject(raw: string): string {
|
|
109
|
+
return raw.trim().replace(/^["'`]+|["'`]+$/g, "").replace(/[.,;:]+$/, "").toLowerCase();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Per-operation object patterns, each with the capture group that holds it. */
|
|
113
|
+
const OBJECT_PATTERNS: Readonly<Record<string, readonly RegExp[]>> = {
|
|
114
|
+
"prod-data-destroy": [
|
|
115
|
+
/\b(?:tabelle|table|datenbank|database|schema|collection|index)\s+["'`]?([A-Za-z_][\w.$-]*)/gi,
|
|
116
|
+
/\bs3:\/\/([\w.-]+)/gi,
|
|
117
|
+
/\b(?:bucket)\s+["'`]?([A-Za-z_][\w.-]*)/gi,
|
|
118
|
+
],
|
|
119
|
+
"external-send": [
|
|
120
|
+
/([A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,})/g,
|
|
121
|
+
/\bhttps?:\/\/([^\s/"'`]+)/gi,
|
|
122
|
+
/\b(?:an\s+(?:die|den|alle)|to\s+(?:the|all))\s+([A-Za-z][\w-]*)/gi,
|
|
123
|
+
],
|
|
124
|
+
"money-movement": [
|
|
125
|
+
/(\d[\d.,]*)\s*(?:€|eur\b|euro\b|\$|usd\b)/gi,
|
|
126
|
+
/(?:€|\$)\s*(\d[\d.,]*)/g,
|
|
127
|
+
/\b(?:kunde|kunden|customer|account|konto)\s+["'`]?([A-Za-z0-9][\w.-]*)/gi,
|
|
128
|
+
],
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The objects a sentence names for one operation.
|
|
133
|
+
*
|
|
134
|
+
* Returns `[]` when the prose names none — which is a real answer and the one
|
|
135
|
+
* `conformance_scan` reads as "the record does not name this object". An empty
|
|
136
|
+
* list is never widened into a wildcard.
|
|
137
|
+
*/
|
|
138
|
+
export function extractObjects(op: GitOp, text: string): string[] {
|
|
139
|
+
const patterns = OBJECT_PATTERNS[op];
|
|
140
|
+
if (!patterns) return [];
|
|
141
|
+
const out: string[] = [];
|
|
142
|
+
for (const re of patterns) {
|
|
143
|
+
const local = new RegExp(re.source, re.flags.includes("g") ? re.flags : `${re.flags}g`);
|
|
144
|
+
let m: RegExpExecArray | null;
|
|
145
|
+
while ((m = local.exec(text)) !== null) {
|
|
146
|
+
const value = _normalizeObject(m[1] ?? "");
|
|
147
|
+
if (value && !OBJECT_STOP_WORDS.has(value) && !out.includes(value)) out.push(value);
|
|
148
|
+
if (m.index === local.lastIndex) local.lastIndex++;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
@@ -68,6 +68,13 @@ export const ALLOWLIST_PREFIXES: readonly (readonly string[])[] = [
|
|
|
68
68
|
// a name. Read-only, deterministic, model-free, and already run in CI for its
|
|
69
69
|
// own reasons — the same class as its neighbours here.
|
|
70
70
|
['check_no_external_sources'],
|
|
71
|
+
// Compares two COMMITTED files — the skill-activation census record against
|
|
72
|
+
// the sentence `docs/CLAIMS.md` publishes — so its exit code IS the verdict
|
|
73
|
+
// for that claim rather than evidence that some report exists. The census
|
|
74
|
+
// itself is deliberately NOT here: it reads a transcript store under `$HOME`
|
|
75
|
+
// that is absent on a runner, which would make the verdict an artifact of
|
|
76
|
+
// where it ran.
|
|
77
|
+
['check_skill_activation_claim'],
|
|
71
78
|
['check_token_regression'],
|
|
72
79
|
['lint_agent_security'],
|
|
73
80
|
['measure_lexical_ranking'],
|
|
@@ -356,7 +356,7 @@ function as_posix(p: string): string {
|
|
|
356
356
|
*
|
|
357
357
|
* `dry_run=true` computes and returns the would-delete set (only paths that
|
|
358
358
|
* currently exist on disk) WITHOUT unlinking anything or pruning empty
|
|
359
|
-
* directories — the preview surface for `install
|
|
359
|
+
* directories — the preview surface for `agent-config install --dry-run`. The selection
|
|
360
360
|
* logic (orphan diff, containment proof, directory guard) is identical to the
|
|
361
361
|
* live path, so the preview is exact.
|
|
362
362
|
*/
|
|
@@ -456,7 +456,7 @@ export function reap_stale(
|
|
|
456
456
|
*
|
|
457
457
|
* `dry_run=true` returns the would-delete set (tagged orphans actually present
|
|
458
458
|
* on disk) WITHOUT unlinking or pruning — the preview surface for
|
|
459
|
-
* `install
|
|
459
|
+
* `agent-config install --dry-run`. Selection logic is identical to the live path.
|
|
460
460
|
*/
|
|
461
461
|
export function reap_tagged_orphans(
|
|
462
462
|
anchor: string,
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* host_permission_checks — report the host permission settings that produce
|
|
3
|
+
* confirmation prompts, and write none of them.
|
|
4
|
+
*
|
|
5
|
+
* WHY REPORT AND NOT WRITE. Three settings decide how often a normal coding
|
|
6
|
+
* task is interrupted, and all three belong to the consumer:
|
|
7
|
+
* `permissions.deny` rules that cover reads, the working tree's absence from
|
|
8
|
+
* `permissions.additionalDirectories`, and `permissions.defaultMode`. Writing
|
|
9
|
+
* any of them would be this package deciding a consumer's permission posture
|
|
10
|
+
* from inside their repository, which is the one thing a permission surface
|
|
11
|
+
* must never do quietly. `src/templates/consumer-settings/claude-settings.json`
|
|
12
|
+
* carries `enabledPlugins` and `hooks` and zero `permissions` keys; this check
|
|
13
|
+
* exists so that stays true and the consumer still gets the answer.
|
|
14
|
+
*
|
|
15
|
+
* WHY IT NAMES ALL THREE EVEN WHEN ALL THREE ARE ABSENT. Absent is a real
|
|
16
|
+
* answer and the most common one. A check that goes silent on the default
|
|
17
|
+
* project reports "nothing found" and "I did not look" identically, which is
|
|
18
|
+
* the measured-zero-versus-not-measured failure this tree's own report
|
|
19
|
+
* contracts argue against. So each setting is named on every run, with its
|
|
20
|
+
* resolved value or the word absent.
|
|
21
|
+
*
|
|
22
|
+
* READ ONLY, AND FAILURE-TOLERANT. An unreadable or malformed settings file is
|
|
23
|
+
* reported, never repaired: a consumer's JSON is theirs, and a doctor that
|
|
24
|
+
* rewrites what it came to inspect is not a doctor.
|
|
25
|
+
*/
|
|
26
|
+
import * as fs from "node:fs";
|
|
27
|
+
import * as os from "node:os";
|
|
28
|
+
import * as path from "node:path";
|
|
29
|
+
|
|
30
|
+
import type { WiringCheck } from "./runtime_wiring_checks.js";
|
|
31
|
+
|
|
32
|
+
/** Injected so a test never reads the developer's own home directory. */
|
|
33
|
+
export interface HostPermissionDeps {
|
|
34
|
+
projectRoot: string;
|
|
35
|
+
homeDir: string;
|
|
36
|
+
readFile: (p: string) => string | null;
|
|
37
|
+
isDirectory: (p: string) => boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The permission block as the host writes it; every field optional. */
|
|
41
|
+
interface PermissionBlock {
|
|
42
|
+
allow?: unknown;
|
|
43
|
+
deny?: unknown;
|
|
44
|
+
ask?: unknown;
|
|
45
|
+
additionalDirectories?: unknown;
|
|
46
|
+
defaultMode?: unknown;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** One settings layer, in the host's own precedence order. */
|
|
50
|
+
export interface SettingsLayer {
|
|
51
|
+
label: string;
|
|
52
|
+
file: string;
|
|
53
|
+
present: boolean;
|
|
54
|
+
unreadable: boolean;
|
|
55
|
+
permissions: PermissionBlock;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Tool names whose every documented effect is a read, plus the read-shaped
|
|
60
|
+
* `Bash(<cmd>:*)` heads. A deny covering one of these is the class that turns a
|
|
61
|
+
* category-A call back into a prompt, which is why it is worth naming
|
|
62
|
+
* separately from a deny on a write.
|
|
63
|
+
*/
|
|
64
|
+
const READ_SHAPED = [
|
|
65
|
+
"Read", "Glob", "Grep", "LS", "NotebookRead",
|
|
66
|
+
"Bash(cat", "Bash(ls", "Bash(head", "Bash(tail", "Bash(grep",
|
|
67
|
+
"Bash(rg", "Bash(find", "Bash(wc", "Bash(git status", "Bash(git log",
|
|
68
|
+
"Bash(git diff", "Bash(git show",
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
/** The four `defaultMode` values probed from Claude Code 2.1.263. */
|
|
72
|
+
const KNOWN_MODES = ["default", "acceptEdits", "plan", "bypassPermissions"];
|
|
73
|
+
|
|
74
|
+
function _strings(value: unknown): string[] {
|
|
75
|
+
return Array.isArray(value) ? value.filter((v): v is string => typeof v === "string") : [];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Default deps — the only place the real filesystem is touched. */
|
|
79
|
+
export function defaultHostPermissionDeps(projectRoot: string): HostPermissionDeps {
|
|
80
|
+
return {
|
|
81
|
+
projectRoot,
|
|
82
|
+
homeDir: os.homedir(),
|
|
83
|
+
readFile: (p) => {
|
|
84
|
+
try {
|
|
85
|
+
return fs.readFileSync(p, "utf8");
|
|
86
|
+
} catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
isDirectory: (p) => {
|
|
91
|
+
try {
|
|
92
|
+
return fs.statSync(p).isDirectory();
|
|
93
|
+
} catch {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Resolve the three settings layers the host reads, in its precedence order.
|
|
102
|
+
*
|
|
103
|
+
* There was no resolver for this before — `security_audit_config.ts` is the
|
|
104
|
+
* only place all three paths appear together, and only as glob strings — so
|
|
105
|
+
* this is the first code in the tree that reads `permissions` at all.
|
|
106
|
+
*/
|
|
107
|
+
export function readSettingsLayers(deps: HostPermissionDeps): SettingsLayer[] {
|
|
108
|
+
const candidates: Array<[string, string]> = [
|
|
109
|
+
["user-global", path.join(deps.homeDir, ".claude", "settings.json")],
|
|
110
|
+
["project", path.join(deps.projectRoot, ".claude", "settings.json")],
|
|
111
|
+
["project-local", path.join(deps.projectRoot, ".claude", "settings.local.json")],
|
|
112
|
+
];
|
|
113
|
+
return candidates.map(([label, file]) => {
|
|
114
|
+
const raw = deps.readFile(file);
|
|
115
|
+
if (raw === null) {
|
|
116
|
+
return { label, file, present: false, unreadable: false, permissions: {} };
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
const parsed = JSON.parse(raw) as Record<string, unknown>;
|
|
120
|
+
const perms = parsed["permissions"];
|
|
121
|
+
return {
|
|
122
|
+
label,
|
|
123
|
+
file,
|
|
124
|
+
present: true,
|
|
125
|
+
unreadable: false,
|
|
126
|
+
permissions:
|
|
127
|
+
perms !== null && typeof perms === "object" ? (perms as PermissionBlock) : {},
|
|
128
|
+
};
|
|
129
|
+
} catch {
|
|
130
|
+
return { label, file, present: true, unreadable: true, permissions: {} };
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Deny rules that cover a read, with the layer each came from. */
|
|
136
|
+
export function readDenyRules(layers: readonly SettingsLayer[]): Array<[string, string]> {
|
|
137
|
+
const hits: Array<[string, string]> = [];
|
|
138
|
+
for (const layer of layers) {
|
|
139
|
+
for (const rule of _strings(layer.permissions.deny)) {
|
|
140
|
+
if (READ_SHAPED.some((prefix) => rule.startsWith(prefix))) {
|
|
141
|
+
hits.push([layer.label, rule]);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return hits;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Every `additionalDirectories` entry across the layers; the host unions them. */
|
|
149
|
+
export function additionalDirectories(layers: readonly SettingsLayer[]): string[] {
|
|
150
|
+
const seen: string[] = [];
|
|
151
|
+
for (const layer of layers) {
|
|
152
|
+
for (const dir of _strings(layer.permissions.additionalDirectories)) {
|
|
153
|
+
if (!seen.includes(dir)) seen.push(dir);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return seen;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** The winning `defaultMode`, or `null` when no layer sets one. */
|
|
160
|
+
export function resolvedDefaultMode(layers: readonly SettingsLayer[]): string | null {
|
|
161
|
+
let winner: string | null = null;
|
|
162
|
+
for (const layer of layers) {
|
|
163
|
+
const mode = layer.permissions.defaultMode;
|
|
164
|
+
if (typeof mode === "string" && mode.trim()) winner = mode.trim();
|
|
165
|
+
}
|
|
166
|
+
return winner;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Is the project root a LINKED worktree?
|
|
171
|
+
*
|
|
172
|
+
* A linked worktree's `.git` is a file holding a `gitdir:` pointer, never a
|
|
173
|
+
* directory. It matters here because a worktree root is a working directory the
|
|
174
|
+
* host was not started in, which is exactly the case
|
|
175
|
+
* `additionalDirectories` exists to cover — and the case a consumer is least
|
|
176
|
+
* likely to have thought about.
|
|
177
|
+
*/
|
|
178
|
+
export function isLinkedWorktree(deps: HostPermissionDeps): boolean {
|
|
179
|
+
const dotGit = path.join(deps.projectRoot, ".git");
|
|
180
|
+
const raw = deps.readFile(dotGit);
|
|
181
|
+
if (raw === null) return false;
|
|
182
|
+
return !deps.isDirectory(dotGit) && raw.trimStart().startsWith("gitdir:");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** The copyable snippet — the consumer pastes it, this package never writes it. */
|
|
186
|
+
export function permissionSnippet(projectRoot: string): string {
|
|
187
|
+
return JSON.stringify(
|
|
188
|
+
{ permissions: { additionalDirectories: [projectRoot], defaultMode: "default" } },
|
|
189
|
+
null,
|
|
190
|
+
2,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* The doctor row.
|
|
196
|
+
*
|
|
197
|
+
* `warn` when something reportable was found, `ok` otherwise. Never `fail`:
|
|
198
|
+
* these are the consumer's own choices, and a doctor that fails a build over a
|
|
199
|
+
* setting it is forbidden to write would be asking for a change it will not
|
|
200
|
+
* make.
|
|
201
|
+
*/
|
|
202
|
+
export function checkHostPermissionSettings(deps: HostPermissionDeps): WiringCheck {
|
|
203
|
+
const layers = readSettingsLayers(deps);
|
|
204
|
+
const unreadable = layers.filter((l) => l.unreadable);
|
|
205
|
+
const denies = readDenyRules(layers);
|
|
206
|
+
const dirs = additionalDirectories(layers);
|
|
207
|
+
const mode = resolvedDefaultMode(layers);
|
|
208
|
+
const worktree = isLinkedWorktree(deps);
|
|
209
|
+
const rootListed = dirs.some(
|
|
210
|
+
(d) => path.resolve(d) === path.resolve(deps.projectRoot),
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
const lines: string[] = [];
|
|
214
|
+
lines.push(
|
|
215
|
+
denies.length === 0
|
|
216
|
+
? "read-deny rules: absent — no permissions.deny rule covers a read"
|
|
217
|
+
: `read-deny rules: ${denies.length} — ` +
|
|
218
|
+
denies.map(([layer, rule]) => `${rule} (${layer})`).join(", "),
|
|
219
|
+
);
|
|
220
|
+
lines.push(
|
|
221
|
+
rootListed
|
|
222
|
+
? `working tree in additionalDirectories: present (${deps.projectRoot})`
|
|
223
|
+
: `working tree in additionalDirectories: absent (${deps.projectRoot}` +
|
|
224
|
+
`${worktree ? ", a linked worktree" : ""})`,
|
|
225
|
+
);
|
|
226
|
+
lines.push(
|
|
227
|
+
mode === null
|
|
228
|
+
? "permissions.defaultMode: absent — the host's own built-in default applies"
|
|
229
|
+
: `permissions.defaultMode: ${mode}` +
|
|
230
|
+
(KNOWN_MODES.includes(mode) ? "" : " — not a value this tree has probed"),
|
|
231
|
+
);
|
|
232
|
+
for (const layer of unreadable) {
|
|
233
|
+
lines.push(`unreadable: ${layer.file} (${layer.label}) is not valid JSON`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// A linked worktree unlisted in additionalDirectories is the one finding
|
|
237
|
+
// worth surfacing on its own: the host was started somewhere else, so every
|
|
238
|
+
// call into this tree is a call into a directory it was not granted.
|
|
239
|
+
const reportable =
|
|
240
|
+
denies.length > 0 || unreadable.length > 0 || (worktree && !rootListed);
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
id: "host-permission-settings",
|
|
244
|
+
status: reportable ? "warn" : "ok",
|
|
245
|
+
message: lines.join("; "),
|
|
246
|
+
remedy:
|
|
247
|
+
"agent-config never writes permissions — paste this into " +
|
|
248
|
+
`${path.join(deps.projectRoot, ".claude", "settings.local.json")} yourself if you want it: ` +
|
|
249
|
+
permissionSnippet(deps.projectRoot).replace(/\s+/g, " "),
|
|
250
|
+
};
|
|
251
|
+
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* - the repo generator (`condense.py generate_claude_skills`), which
|
|
12
12
|
* builds the package's own `.claude/skills/` tree, and
|
|
13
13
|
* - the consumer install finalizer
|
|
14
|
-
* (`
|
|
14
|
+
* (the installer's `finalize_claude_model_tiers`), which rewrites the
|
|
15
15
|
* installed `.claude/skills/` tree so Claude Code performs the
|
|
16
16
|
* per-turn model switch on a consumer with `model.auto_switch: auto`.
|
|
17
17
|
*
|
|
@@ -275,7 +275,7 @@ export function slot_frequency(platform: string, slot: Slot): Frequency {
|
|
|
275
275
|
}
|
|
276
276
|
|
|
277
277
|
/**
|
|
278
|
-
* Platforms
|
|
278
|
+
* Platforms this package binds nothing on.
|
|
279
279
|
*
|
|
280
280
|
* Copilot declares `fallback_only: true` in the manifest — concerns route
|
|
281
281
|
* through rule-only fallback and the dispatcher no-ops. Every hook-declared
|
|
@@ -294,7 +294,7 @@ export function is_hook_capable(platform_block: Record<string, unknown>): boolea
|
|
|
294
294
|
export interface PlatformBinding {
|
|
295
295
|
/** Platform id → slot → concern ids bound in that slot. */
|
|
296
296
|
slots: Map<string, Map<Slot, string[]>>;
|
|
297
|
-
/** Platform ids
|
|
297
|
+
/** Platform ids declared `fallback_only` — this package binds nothing there. */
|
|
298
298
|
fallback_only: Set<string>;
|
|
299
299
|
}
|
|
300
300
|
|
|
@@ -29,6 +29,10 @@ import {
|
|
|
29
29
|
CURATED_HEAD_INSTRUCTION,
|
|
30
30
|
MIX_RESPONSE_MARKER,
|
|
31
31
|
MIX_RESPONSE_PLACEHOLDERS,
|
|
32
|
+
PROMISE_OUTCOMES,
|
|
33
|
+
PROMISE_PHRASE,
|
|
34
|
+
PROMISE_READBACK_MARKER,
|
|
35
|
+
extract_changelog_section,
|
|
32
36
|
} from './release_material.js';
|
|
33
37
|
|
|
34
38
|
/** The five curated labels, in the order an operator reads them. */
|
|
@@ -546,6 +550,95 @@ export function mix_response_blockers(
|
|
|
546
550
|
return [];
|
|
547
551
|
}
|
|
548
552
|
|
|
553
|
+
/**
|
|
554
|
+
* The promise the PREVIOUS release head made, or null when it made none.
|
|
555
|
+
*
|
|
556
|
+
* Scoped to that section's governance-response block, not to its whole body: a
|
|
557
|
+
* product line that happens to contain the phrase is not a promise the project
|
|
558
|
+
* made about the next cycle.
|
|
559
|
+
*/
|
|
560
|
+
export function previous_promise(changelogText: string, previousVersion: string): string | null {
|
|
561
|
+
const section = extract_changelog_section(changelogText, previousVersion);
|
|
562
|
+
if (section === null) return null;
|
|
563
|
+
const idx = section.body.indexOf(MIX_RESPONSE_MARKER);
|
|
564
|
+
if (idx === -1) return null;
|
|
565
|
+
const block = mix_response_block(section.body, idx);
|
|
566
|
+
return block.includes(PROMISE_PHRASE) ? block : null;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* The ONE predicate for "did this head answer the previous head's promise?".
|
|
571
|
+
*
|
|
572
|
+
* The defect: `_lib/release_material.ts` generates a `Next cycle ships …` line
|
|
573
|
+
* and nothing has ever read the previous one back, so an unmet promise can be
|
|
574
|
+
* restated indefinitely at zero cost — the debt is booked and never called. A
|
|
575
|
+
* promise nobody reads is not a commitment, it is a sentence.
|
|
576
|
+
*
|
|
577
|
+
* Three outcomes are accepted and one of them must be chosen, which is the
|
|
578
|
+
* whole mechanism: it does not require the promise to have been KEPT, and no
|
|
579
|
+
* ratio, deadline or count is enforced here. Refusing an unkept promise would
|
|
580
|
+
* make the honest answer the expensive one and teach an author to promise less
|
|
581
|
+
* rather than to answer truthfully.
|
|
582
|
+
*
|
|
583
|
+
* Length floor and placeholder scan reuse the governance-response discipline
|
|
584
|
+
* for the same reason it exists there: a bare marker, or the writer's own
|
|
585
|
+
* scaffolding, is not an answer a human wrote.
|
|
586
|
+
*/
|
|
587
|
+
export function promise_readback_blockers(
|
|
588
|
+
sectionBody: string,
|
|
589
|
+
version: string,
|
|
590
|
+
previousVersion: string,
|
|
591
|
+
promise: string | null,
|
|
592
|
+
where = '`main`',
|
|
593
|
+
): string[] {
|
|
594
|
+
if (promise === null) return [];
|
|
595
|
+
const idx = sectionBody.indexOf(PROMISE_READBACK_MARKER);
|
|
596
|
+
if (idx === -1) {
|
|
597
|
+
return [
|
|
598
|
+
`the ${version} section does not answer the ${previousVersion} head's ` +
|
|
599
|
+
`\`${PROMISE_PHRASE}\` promise. Add one \`> ${PROMISE_READBACK_MARKER}\` line ` +
|
|
600
|
+
`immediately under the curated head on ${where}, naming the promise and whether ` +
|
|
601
|
+
`it ${PROMISE_OUTCOMES.join(', ')} (the last one with its reason).`,
|
|
602
|
+
];
|
|
603
|
+
}
|
|
604
|
+
const block = mix_response_block(sectionBody, idx);
|
|
605
|
+
const left = MIX_RESPONSE_PLACEHOLDERS.filter((t) => block.includes(t));
|
|
606
|
+
if (left.length > 0) {
|
|
607
|
+
return [
|
|
608
|
+
`the ${version} previous-cycle answer still carries the writer's placeholder(s) ` +
|
|
609
|
+
`(${left.map((t) => `\`${t}\``).join(', ')}). \`CHANGELOG.md\` is published to npm. ` +
|
|
610
|
+
`Write the outcome on ${where} and re-run.`,
|
|
611
|
+
];
|
|
612
|
+
}
|
|
613
|
+
const lower = block.toLowerCase();
|
|
614
|
+
const named = PROMISE_OUTCOMES.filter((o) => lower.includes(o));
|
|
615
|
+
if (named.length === 0) {
|
|
616
|
+
return [
|
|
617
|
+
`the ${version} previous-cycle answer names no outcome. It has to say which of ` +
|
|
618
|
+
`${PROMISE_OUTCOMES.join(', ')} applies to the ${previousVersion} promise — an ` +
|
|
619
|
+
'answer that restates the promise without resolving it is the failure this reads ' +
|
|
620
|
+
'back for.',
|
|
621
|
+
];
|
|
622
|
+
}
|
|
623
|
+
if (readback_answer(block).length < MIX_RESPONSE_MIN_CHARS) {
|
|
624
|
+
return [
|
|
625
|
+
`the ${version} previous-cycle answer is shorter than a written answer ` +
|
|
626
|
+
`(${String(MIX_RESPONSE_MIN_CHARS)} characters after the marker). Name what was ` +
|
|
627
|
+
`promised and what became of it on ${where}, not the outcome word alone.`,
|
|
628
|
+
];
|
|
629
|
+
}
|
|
630
|
+
return [];
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/** What the author wrote under the read-back marker, marker and quoting removed. */
|
|
634
|
+
export function readback_answer(block: string): string {
|
|
635
|
+
return block
|
|
636
|
+
.replace(PROMISE_READBACK_MARKER, '')
|
|
637
|
+
.replace(/^[>\s.]+|[>\s.]+$/gu, '')
|
|
638
|
+
.replace(/[>\s]+/gu, ' ')
|
|
639
|
+
.trim();
|
|
640
|
+
}
|
|
641
|
+
|
|
549
642
|
/**
|
|
550
643
|
* The whole response block: the marker's line plus every blockquote line under it.
|
|
551
644
|
*
|