@event4u/agent-config 9.16.0 → 9.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +53 -0
- package/README.md +1 -1
- package/dist/agent-src/commands/agent-handoff.md +100 -48
- package/dist/agent-src/commands/commit.md +1 -1
- package/dist/agent-src/commands/condense.md +19 -64
- package/dist/agent-src/commands/evals/agent-handoff.json +13 -1
- package/dist/agent-src/commands/memory/promote.md +14 -1
- package/dist/agent-src/commands/mode.md +9 -5
- package/dist/agent-src/commands/optimize/deep.md +72 -11
- package/dist/agent-src/contexts/authority/commit-mechanics.md +5 -5
- package/dist/agent-src/contexts/communication/rules-auto/guidelines-mechanics.md +1 -0
- package/dist/agent-src/contexts/communication/rules-auto/skill-quality-mechanics.md +1 -1
- package/dist/agent-src/contexts/communication/rules-auto/think-before-action-mechanics.md +6 -1
- package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +4 -1
- package/dist/agent-src/contexts/execution/autonomy-mechanics.md +27 -1
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +5 -2
- package/dist/agent-src/profiles/agency.yml +3 -3
- package/dist/agent-src/profiles/content_creator.yml +2 -2
- package/dist/agent-src/profiles/developer.yml +2 -2
- package/dist/agent-src/profiles/finance.yml +1 -1
- package/dist/agent-src/profiles/founder.yml +2 -2
- package/dist/agent-src/profiles/ops.yml +2 -2
- package/dist/agent-src/rules/architecture.md +2 -2
- package/dist/agent-src/rules/autonomous-execution.md +1 -1
- package/dist/agent-src/rules/context-hygiene.md +3 -2
- package/dist/agent-src/rules/domain-safety-pii.md +1 -1
- package/dist/agent-src/rules/output-discipline.md +9 -2
- package/dist/agent-src/rules/token-budget-discipline.md +2 -2
- package/dist/agent-src/rules/user-interaction.md +3 -4
- package/dist/agent-src/skills/build-buy-partner/SKILL.md +1 -1
- package/dist/agent-src/skills/copilot-agents-optimization/SKILL.md +4 -4
- package/dist/agent-src/skills/copilot-config/SKILL.md +1 -1
- package/dist/agent-src/skills/deep-reading-analyst/SKILL.md +7 -4
- package/dist/agent-src/skills/description-assist/SKILL.md +1 -1
- package/dist/agent-src/skills/fe-design/SKILL.md +1 -1
- package/dist/agent-src/skills/market-entry-analysis/SKILL.md +1 -1
- package/dist/agent-src/skills/persona-writing/SKILL.md +2 -2
- package/dist/agent-src/skills/readme-reviewer/SKILL.md +2 -1
- package/dist/agent-src/skills/rule-writing/SKILL.md +4 -2
- package/dist/agent-src/skills/skill-reviewer/SKILL.md +1 -1
- package/dist/agent-src/skills/skill-writing/SKILL.md +2 -2
- package/dist/agent-src/skills/tailwind-engineer/SKILL.md +3 -1
- package/dist/agent-src/skills/token-optimizer/SKILL.md +2 -2
- package/dist/agent-src/skills/ui-component-architect/SKILL.md +6 -3
- package/dist/agent-src/templates/agent-settings.md +13 -15
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +6 -2
- package/dist/agent-src/templates/persona.md +2 -2
- package/dist/agent-src/templates/roadmaps.md +1 -1
- package/dist/agent-src/templates/rule.md +1 -1
- package/dist/agent-src/templates/skill.md +1 -1
- package/dist/cli/registry.js +1 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-ZXXJLL5K.js → chunk-BSLUHV2F.js} +127 -1487
- package/dist/cli-delegate/{chunk-H574KOUH.js → chunk-IMSXPRVH.js} +24 -1
- package/dist/cli-delegate/chunk-VSYT7OHO.js +1382 -0
- package/dist/cli-delegate/chunk-XGA2NTU4.js +3727 -0
- package/dist/cli-delegate/cmd_conformance.js +4 -3
- package/dist/cli-delegate/cmd_converge.js +3 -3
- package/dist/cli-delegate/cmd_doctor.js +3 -2
- package/dist/cli-delegate/cmd_export.js +4 -4
- package/dist/cli-delegate/cmd_handoff.js +3194 -0
- package/dist/cli-delegate/cmd_memory_get.js +35 -3754
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_prune.js +4 -4
- package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
- package/dist/cli-delegate/cmd_sync.js +4 -4
- package/dist/cli-delegate/cmd_uninstall.js +4 -4
- package/dist/cli-delegate/cmd_update.js +3 -3
- package/dist/cli-delegate/cmd_upgrade.js +3 -3
- package/dist/cli-delegate/cmd_validate.js +4 -4
- package/dist/cli-delegate/cmd_versions.js +3 -3
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +31 -31
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/dispatch.js +602 -507
- package/dist/install/install.mjs +23 -0
- package/dist/mcp/awesome-mcp-servers.row.md +1 -1
- package/dist/mcp/mcp-cloudflare-catalogue.json +2 -2
- package/dist/mcp/registry-manifest.json +4 -4
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +25 -3
- package/dist/scripts/_lib/agent_src.js +39 -6
- package/dist/scripts/_lib/agent_src.js.map +1 -1
- package/dist/server/schemas/settings.js +13 -0
- package/dist/server/schemas/settings.js.map +1 -1
- package/docs/guidelines/abstraction-thresholds.md +53 -0
- package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +1 -1
- package/docs/guidelines/agent-infra/asking-and-brevity-examples.md +7 -0
- package/docs/guidelines/agent-infra/context-hygiene-mechanics.md +2 -2
- package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +5 -3
- package/docs/guidelines/agent-infra/size-and-scope.md +8 -2
- package/docs/guidelines/component-oriented-and-oop-development.md +11 -6
- package/docs/guidelines/docs/readme-size-and-splitting.md +5 -2
- package/llms.txt +1 -0
- package/package.json +1 -1
- package/src/config/agent-settings.template.yml +22 -0
- package/src/config/evaluator-budgets.json +8 -11
- package/src/config/gate-coverage.yml +30 -0
- package/src/scripts/_cli/cmd_handoff.ts +405 -0
- package/src/scripts/_cli/handoff_generate.ts +453 -0
- package/src/scripts/_cli/handoff_sessions.ts +435 -0
- package/src/scripts/_dispatch.bash +9 -0
- package/src/scripts/_lib/agent_src.ts +36 -6
- package/src/scripts/_lib/release_material.ts +156 -0
- package/src/scripts/check_condensation.ts +2 -1
- package/src/scripts/check_finding_dispositions.ts +278 -0
- package/src/scripts/check_pack_size.ts +4 -3
- package/src/scripts/check_references.ts +1 -0
- package/src/scripts/check_release_highlights.ts +300 -0
- package/src/scripts/check_release_pr_shape.ts +4 -0
- package/src/scripts/check_release_surface_equality.ts +132 -0
- package/src/scripts/check_reply_consistency.ts +187 -138
- package/src/scripts/handoff_context_hook.ts +148 -0
- package/src/scripts/hook_manifest.yaml +17 -7
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/lint_abstraction_thresholds.ts +325 -0
- package/src/scripts/lint_framework_leakage_allowlist.json +2 -2
- package/src/scripts/lint_profile_personas.ts +153 -0
- package/src/scripts/lint_token_budget_discipline.ts +148 -0
- package/src/scripts/release.ts +156 -32
- package/src/scripts/release_drill.ts +491 -0
- package/src/scripts/schemas/command.schema.json +33 -0
- package/src/scripts/self_review_gate.ts +41 -3
- package/src/scripts/skill_linter.ts +3 -1
- package/src/scripts/smoke/router.sh +1 -1
- package/src/scripts/update_counts.ts +60 -12
- package/src/server/schemas/settings.ts +23 -0
- package/dist/cli-delegate/{chunk-XX6TOPD7.js → chunk-WR6GTRHJ.js} +3 -3
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Release-surface equality gate (release-truth Phase 1).
|
|
3
|
+
*
|
|
4
|
+
* Asserts `normalized(release-PR body) == normalized(CHANGELOG entry)` for
|
|
5
|
+
* the version a release PR carries — whitespace/anchor normalization only,
|
|
6
|
+
* never "similar". The recorded failure this closes: at 9.14.0 the PR body
|
|
7
|
+
* and the merged changelog disagreed on scope and test count because the
|
|
8
|
+
* surfaces were rendered at different times; with `release.ts` deriving every
|
|
9
|
+
* surface from the changelog section this gate proves the derivation held at
|
|
10
|
+
* the head CI actually sees.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* check_release_surface_equality --pr <number> # fetch body via gh
|
|
14
|
+
* check_release_surface_equality --body-file <path> # fixture / local
|
|
15
|
+
* [--changelog <path>] [--version <X.Y.Z>]
|
|
16
|
+
*
|
|
17
|
+
* `--version` defaults to package.json's version (the release PR has already
|
|
18
|
+
* bumped it). Exit codes: 0 equal · 1 divergence · 2 usage/extraction error.
|
|
19
|
+
*/
|
|
20
|
+
import { spawnSync } from 'node:child_process';
|
|
21
|
+
import * as fs from 'node:fs';
|
|
22
|
+
import * as path from 'node:path';
|
|
23
|
+
import process from 'node:process';
|
|
24
|
+
import { fileURLToPath } from 'node:url';
|
|
25
|
+
|
|
26
|
+
import {
|
|
27
|
+
extract_changelog_section,
|
|
28
|
+
strip_pr_wrapper,
|
|
29
|
+
surface_divergence,
|
|
30
|
+
} from './_lib/release_material.js';
|
|
31
|
+
|
|
32
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
33
|
+
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
34
|
+
|
|
35
|
+
export interface EqualityInput {
|
|
36
|
+
prBody: string;
|
|
37
|
+
changelogText: string;
|
|
38
|
+
version: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface EqualityResult {
|
|
42
|
+
ok: boolean;
|
|
43
|
+
reason: string | null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Pure core — the CLI and the release-validation job both run exactly this. */
|
|
47
|
+
export function check_surface_equality(input: EqualityInput): EqualityResult {
|
|
48
|
+
const section = extract_changelog_section(input.changelogText, input.version);
|
|
49
|
+
if (!section) {
|
|
50
|
+
return {
|
|
51
|
+
ok: false,
|
|
52
|
+
reason: `CHANGELOG carries no section for ${input.version}`,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const middle = strip_pr_wrapper(input.prBody, input.version);
|
|
56
|
+
if (middle === null) {
|
|
57
|
+
return {
|
|
58
|
+
ok: false,
|
|
59
|
+
reason:
|
|
60
|
+
`PR body does not carry the canonical "Release ${input.version}." prefix — ` +
|
|
61
|
+
'it was not generated from the changelog section',
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const divergence = surface_divergence(middle, section.body);
|
|
65
|
+
return divergence === null ? { ok: true, reason: null } : { ok: false, reason: divergence };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function _pr_body(prNumber: string): string {
|
|
69
|
+
const r = spawnSync('gh', ['pr', 'view', prNumber, '--json', 'body', '-q', '.body'], {
|
|
70
|
+
encoding: 'utf-8',
|
|
71
|
+
});
|
|
72
|
+
if (r.status !== 0) {
|
|
73
|
+
process.stderr.write(`gh pr view ${prNumber} failed: ${r.stderr}\n`);
|
|
74
|
+
process.exit(2);
|
|
75
|
+
}
|
|
76
|
+
return r.stdout;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function main(argv: readonly string[]): number {
|
|
80
|
+
let pr: string | null = null;
|
|
81
|
+
let bodyFile: string | null = null;
|
|
82
|
+
let changelogPath = path.join(REPO_ROOT, 'CHANGELOG.md');
|
|
83
|
+
let version: string | null = null;
|
|
84
|
+
for (let i = 0; i < argv.length; i++) {
|
|
85
|
+
const a = argv[i];
|
|
86
|
+
if (a === '--pr') pr = argv[++i] ?? null;
|
|
87
|
+
else if (a === '--body-file') bodyFile = argv[++i] ?? null;
|
|
88
|
+
else if (a === '--changelog') changelogPath = argv[++i] ?? changelogPath;
|
|
89
|
+
else if (a === '--version') version = argv[++i] ?? null;
|
|
90
|
+
else {
|
|
91
|
+
process.stderr.write(`unknown argument: ${a}\n`);
|
|
92
|
+
return 2;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if ((pr === null) === (bodyFile === null)) {
|
|
96
|
+
process.stderr.write('exactly one of --pr / --body-file is required\n');
|
|
97
|
+
return 2;
|
|
98
|
+
}
|
|
99
|
+
if (version === null) {
|
|
100
|
+
const pkg = JSON.parse(
|
|
101
|
+
fs.readFileSync(path.join(REPO_ROOT, 'package.json'), 'utf-8'),
|
|
102
|
+
) as Record<string, unknown>;
|
|
103
|
+
version = String(pkg['version']);
|
|
104
|
+
}
|
|
105
|
+
const prBody = pr !== null ? _pr_body(pr) : fs.readFileSync(bodyFile!, 'utf-8');
|
|
106
|
+
const changelogText = fs.readFileSync(changelogPath, 'utf-8');
|
|
107
|
+
const result = check_surface_equality({ prBody, changelogText, version });
|
|
108
|
+
if (result.ok) {
|
|
109
|
+
process.stdout.write(`✅ release surfaces equal for ${version}\n`);
|
|
110
|
+
return 0;
|
|
111
|
+
}
|
|
112
|
+
process.stderr.write(
|
|
113
|
+
`❌ release-surface divergence for ${version}: ${result.reason}\n` +
|
|
114
|
+
' The PR body must equal the CHANGELOG entry (whitespace-normalized). ' +
|
|
115
|
+
'Regenerate it: task release -- --resume --yes (or gh pr edit with the section body).\n',
|
|
116
|
+
);
|
|
117
|
+
return 1;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const _isMain = (() => {
|
|
121
|
+
const entry = process.argv[1];
|
|
122
|
+
if (!entry) return false;
|
|
123
|
+
try {
|
|
124
|
+
return fs.realpathSync(entry) === fs.realpathSync(_HERE);
|
|
125
|
+
} catch {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
})();
|
|
129
|
+
|
|
130
|
+
if (_isMain) {
|
|
131
|
+
process.exit(main(process.argv.slice(2)));
|
|
132
|
+
}
|
|
@@ -2,17 +2,42 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* check_reply_consistency.ts — enforce user-interaction.md Iron Laws.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
5
|
+
* Full rewrite (council 2026-08, "fix/extraction-threshold-consistency"):
|
|
6
|
+
* the previous version was a 1:1 port of a retired Python prototype whose
|
|
7
|
+
* lax-by-default, whole-reply semantics contradicted the spec it cites
|
|
8
|
+
* (`src/rules/user-interaction.md` Iron Law 1 +
|
|
9
|
+
* `src/agent-src/contexts/communication/rules-auto/user-interaction-mechanics.md`
|
|
10
|
+
* § Pre-send self-check).
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* Semantics (strict is the DEFAULT — the documented invocation
|
|
13
|
+
* `./scripts-run src/scripts/check_reply_consistency --stdin < draft.md`
|
|
14
|
+
* IS the strict behavior; there is no --strict/--lax flag):
|
|
15
|
+
*
|
|
16
|
+
* 1. Per-block validation. EVERY numbered-options block (>= 2 consecutive
|
|
17
|
+
* numbered lines, optionally blockquoted) must carry exactly ONE
|
|
18
|
+
* `Recommendation:`/`Empfehlung:` line DIRECTLY under it — within the
|
|
19
|
+
* trailing 2 non-blank lines after the block, before any new heading or
|
|
20
|
+
* new options block.
|
|
21
|
+
* 2. Multi-block replies: each block gets its own recommendation line. Two
|
|
22
|
+
* blocks with two (different) recommendation numbers is VALID (mechanics
|
|
23
|
+
* § self-check rule 5); a block without one is a finding.
|
|
24
|
+
* 3. The recommendation number must be within its block's option range.
|
|
25
|
+
* 4. No inline `(recommended)` / `(rec)` / `(empfohlen)` tag on an option
|
|
26
|
+
* line (Iron Law 1 — the option block stays neutral).
|
|
27
|
+
*
|
|
28
|
+
* Known limit — wrong-language label: Iron Law 1 says a wrong-language label
|
|
29
|
+
* (`Recommendation:` for a German user, or vice versa) counts as NO
|
|
30
|
+
* recommendation, but this script cannot know the user's language from the
|
|
31
|
+
* draft alone. Both `Recommendation:` and `Empfehlung:` are therefore
|
|
32
|
+
* accepted in either context; the language match stays a model-side check.
|
|
33
|
+
*
|
|
34
|
+
* `--scan-dir` keeps the legacy-pattern sweep over `.md` trees (inline
|
|
35
|
+
* `(recommended)` tags only — docs legitimately contain numbered lists
|
|
36
|
+
* without recommendation lines, so per-block validation does not apply
|
|
37
|
+
* there).
|
|
38
|
+
*
|
|
39
|
+
* Exit: 0 clean · 1 usage/IO error · 2 findings (consistent with sibling
|
|
40
|
+
* lints, e.g. lint_abstraction_thresholds).
|
|
16
41
|
*/
|
|
17
42
|
|
|
18
43
|
import * as fs from 'node:fs';
|
|
@@ -26,17 +51,36 @@ const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
|
26
51
|
|
|
27
52
|
const QUIET = process.argv.slice(2).includes('--quiet');
|
|
28
53
|
|
|
29
|
-
const OPTION_LINE_RE = /^\s*>?\s*(\d+)\.\s+\S/;
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
const TAG_RE = /\((?:recommended|rec|empfohlen)\)/i;
|
|
54
|
+
export const OPTION_LINE_RE = /^\s*>?\s*(\d+)\.\s+\S/;
|
|
55
|
+
export const REC_LINE_RE = /(?:Recommendation|Empfehlung)\s*:\s*(\d+)\b/i;
|
|
56
|
+
export const TAG_RE = /\((?:recommended|rec|empfohlen)\)/i;
|
|
33
57
|
const CODESPAN_RE = /`[^`\n]*`/g;
|
|
58
|
+
const HEADING_RE = /^#{1,6}\s/;
|
|
59
|
+
const FENCE_RE = /^\s*(```|~~~)/;
|
|
60
|
+
|
|
61
|
+
/** How many non-blank lines after a block may separate it from its recommendation line. */
|
|
62
|
+
export const REC_ADJACENCY_WINDOW = 2;
|
|
63
|
+
|
|
64
|
+
export interface OptionBlock {
|
|
65
|
+
/** 1-based line of the first option line. */
|
|
66
|
+
startLine: number;
|
|
67
|
+
/** 1-based line of the last option line. */
|
|
68
|
+
endLine: number;
|
|
69
|
+
/** Option numbers in order of appearance. */
|
|
70
|
+
numbers: number[];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface Finding {
|
|
74
|
+
/** 1-based line the finding anchors to. */
|
|
75
|
+
line: number;
|
|
76
|
+
message: string;
|
|
77
|
+
}
|
|
34
78
|
|
|
35
79
|
function _strip_codespans(line: string): string {
|
|
36
80
|
return line.replace(CODESPAN_RE, '``');
|
|
37
81
|
}
|
|
38
82
|
|
|
39
|
-
/**
|
|
83
|
+
/** Split on \n / \r\n / \r, dropping a single trailing empty element. */
|
|
40
84
|
function _splitlines(text: string): string[] {
|
|
41
85
|
if (text === '') return [];
|
|
42
86
|
const parts = text.split(/\r\n|\r|\n/);
|
|
@@ -47,87 +91,122 @@ function _splitlines(text: string): string[] {
|
|
|
47
91
|
}
|
|
48
92
|
|
|
49
93
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
94
|
+
* Lines with fenced code blocks blanked out — an example options block quoted
|
|
95
|
+
* inside ``` / ~~~ is illustration, not a live options block.
|
|
52
96
|
*/
|
|
53
|
-
function
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
if (
|
|
97
|
+
export function mask_fences(lines: readonly string[]): string[] {
|
|
98
|
+
const out: string[] = [];
|
|
99
|
+
let fence: string | null = null;
|
|
100
|
+
for (const raw of lines) {
|
|
101
|
+
const m = FENCE_RE.exec(raw);
|
|
102
|
+
if (m) {
|
|
103
|
+
const marker = m[1]!;
|
|
104
|
+
if (fence === null) {
|
|
105
|
+
fence = marker;
|
|
106
|
+
} else if (fence === marker) {
|
|
107
|
+
fence = null;
|
|
108
|
+
}
|
|
109
|
+
out.push('');
|
|
59
110
|
continue;
|
|
60
111
|
}
|
|
61
|
-
|
|
62
|
-
return [idx + 1, raw.trim()];
|
|
63
|
-
}
|
|
112
|
+
out.push(fence === null ? raw : '');
|
|
64
113
|
}
|
|
65
|
-
return
|
|
114
|
+
return out;
|
|
66
115
|
}
|
|
67
116
|
|
|
68
|
-
/**
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
for (const raw of _splitlines(text)) {
|
|
76
|
-
const m = OPTION_LINE_RE.exec(raw);
|
|
117
|
+
/** Consecutive numbered-option lines (>= 2) grouped into blocks. */
|
|
118
|
+
export function find_option_blocks(text: string): OptionBlock[] {
|
|
119
|
+
const lines = mask_fences(_splitlines(text));
|
|
120
|
+
const blocks: OptionBlock[] = [];
|
|
121
|
+
let current: OptionBlock | null = null;
|
|
122
|
+
for (let idx = 0; idx < lines.length; idx++) {
|
|
123
|
+
const m = OPTION_LINE_RE.exec(lines[idx]!);
|
|
77
124
|
if (m) {
|
|
78
|
-
|
|
125
|
+
const num = parseInt(m[1]!, 10);
|
|
126
|
+
if (current === null) {
|
|
127
|
+
current = { startLine: idx + 1, endLine: idx + 1, numbers: [num] };
|
|
128
|
+
} else {
|
|
129
|
+
current.endLine = idx + 1;
|
|
130
|
+
current.numbers.push(num);
|
|
131
|
+
}
|
|
79
132
|
} else {
|
|
80
|
-
if (current.length >= 2) {
|
|
133
|
+
if (current !== null && current.numbers.length >= 2) {
|
|
81
134
|
blocks.push(current);
|
|
82
135
|
}
|
|
83
|
-
current =
|
|
136
|
+
current = null;
|
|
84
137
|
}
|
|
85
138
|
}
|
|
86
|
-
if (current.length >= 2) {
|
|
139
|
+
if (current !== null && current.numbers.length >= 2) {
|
|
87
140
|
blocks.push(current);
|
|
88
141
|
}
|
|
89
142
|
return blocks;
|
|
90
143
|
}
|
|
91
144
|
|
|
92
|
-
/**
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const blocks = find_option_blocks(text);
|
|
101
|
-
const rec_numbers: number[] = [];
|
|
102
|
-
REC_LINE_RE.lastIndex = 0;
|
|
103
|
-
let m: RegExpExecArray | null;
|
|
104
|
-
while ((m = REC_LINE_RE.exec(text)) !== null) {
|
|
105
|
-
rec_numbers.push(parseInt(m[1]!, 10));
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if (blocks.length === 0) {
|
|
109
|
-
return [0, 'ok (no numbered options block)'];
|
|
110
|
-
}
|
|
145
|
+
/**
|
|
146
|
+
* Core check — the exported function tests drive. Returns all findings for
|
|
147
|
+
* one reply draft; empty array = the draft is consistent.
|
|
148
|
+
*/
|
|
149
|
+
export function check_reply(text: string): Finding[] {
|
|
150
|
+
const rawLines = _splitlines(text);
|
|
151
|
+
const lines = mask_fences(rawLines);
|
|
152
|
+
const findings: Finding[] = [];
|
|
111
153
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
154
|
+
// Iron Law 1 — no inline tag on an option line (codespans exempt).
|
|
155
|
+
for (let idx = 0; idx < lines.length; idx++) {
|
|
156
|
+
const stripped = _strip_codespans(lines[idx]!);
|
|
157
|
+
if (OPTION_LINE_RE.test(stripped) && TAG_RE.test(stripped)) {
|
|
158
|
+
findings.push({
|
|
159
|
+
line: idx + 1,
|
|
160
|
+
message: `inline tag on numbered option — ${rawLines[idx]!.trim()}`,
|
|
161
|
+
});
|
|
115
162
|
}
|
|
116
|
-
return [0, 'ok (options without recommendation; non-strict)'];
|
|
117
163
|
}
|
|
118
164
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
return [3, `multiple distinct recommendation numbers: [${distinct.join(', ')}]`];
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const rec_num = distinct[0]!;
|
|
165
|
+
// Per-block: exactly one adjacent recommendation line, number in range.
|
|
166
|
+
const blocks = find_option_blocks(text);
|
|
125
167
|
for (const block of blocks) {
|
|
126
|
-
|
|
127
|
-
|
|
168
|
+
const recs: Array<{ line: number; num: number }> = [];
|
|
169
|
+
let inspected = 0;
|
|
170
|
+
for (let idx = block.endLine; idx < lines.length && inspected < REC_ADJACENCY_WINDOW; idx++) {
|
|
171
|
+
const line = lines[idx]!;
|
|
172
|
+
if (line.trim() === '') {
|
|
173
|
+
continue; // blank lines do not consume the adjacency window
|
|
174
|
+
}
|
|
175
|
+
if (HEADING_RE.test(line) || OPTION_LINE_RE.test(line)) {
|
|
176
|
+
break; // a new heading or a new options block ends this block's window
|
|
177
|
+
}
|
|
178
|
+
inspected += 1;
|
|
179
|
+
const m = REC_LINE_RE.exec(line);
|
|
180
|
+
if (m) {
|
|
181
|
+
recs.push({ line: idx + 1, num: parseInt(m[1]!, 10) });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (recs.length === 0) {
|
|
185
|
+
findings.push({
|
|
186
|
+
line: block.startLine,
|
|
187
|
+
message:
|
|
188
|
+
'numbered options block without a Recommendation:/Empfehlung: line directly under it',
|
|
189
|
+
});
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (recs.length > 1) {
|
|
193
|
+
findings.push({
|
|
194
|
+
line: recs[1]!.line,
|
|
195
|
+
message: `multiple recommendation lines under one options block: [${recs.map((r) => r.num).join(', ')}]`,
|
|
196
|
+
});
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
const rec = recs[0]!;
|
|
200
|
+
if (!block.numbers.includes(rec.num)) {
|
|
201
|
+
findings.push({
|
|
202
|
+
line: rec.line,
|
|
203
|
+
message: `recommendation ${String(rec.num)} not present in the options block above it (options: [${block.numbers.join(', ')}])`,
|
|
204
|
+
});
|
|
128
205
|
}
|
|
129
206
|
}
|
|
130
|
-
|
|
207
|
+
|
|
208
|
+
findings.sort((a, b) => a.line - b.line);
|
|
209
|
+
return findings;
|
|
131
210
|
}
|
|
132
211
|
|
|
133
212
|
function _isDir(p: string): boolean {
|
|
@@ -169,7 +248,8 @@ function _relToPosixOrAbs(child: string, root: string): string {
|
|
|
169
248
|
return rel.split(path.sep).join('/');
|
|
170
249
|
}
|
|
171
250
|
|
|
172
|
-
|
|
251
|
+
/** Legacy-pattern sweep: inline (recommended)-class tags in `.md` trees. */
|
|
252
|
+
export function cmd_scan_dir(root: string): number {
|
|
173
253
|
// If the requested root is the legacy ".agent-src.uncondensed" and it no
|
|
174
254
|
// longer exists (post-monorepo-move), fall back to artefact_roots().
|
|
175
255
|
let roots: string[];
|
|
@@ -179,11 +259,11 @@ function cmd_scan_dir(root: string): number {
|
|
|
179
259
|
roots = artefact_roots();
|
|
180
260
|
if (roots.length === 0) {
|
|
181
261
|
process.stderr.write('error: no artefact roots found (legacy or packages/*)\n');
|
|
182
|
-
return
|
|
262
|
+
return 1;
|
|
183
263
|
}
|
|
184
264
|
} else {
|
|
185
265
|
process.stderr.write(`error: not a directory: ${root}\n`);
|
|
186
|
-
return
|
|
266
|
+
return 1;
|
|
187
267
|
}
|
|
188
268
|
} else {
|
|
189
269
|
roots = [root];
|
|
@@ -204,10 +284,10 @@ function cmd_scan_dir(root: string): number {
|
|
|
204
284
|
}
|
|
205
285
|
if (violations.length > 0) {
|
|
206
286
|
for (const [p, line, snippet] of violations) {
|
|
207
|
-
process.stderr.write(` 🔴 ${p}:${line} — inline-tag — ${snippet}\n`);
|
|
287
|
+
process.stderr.write(` 🔴 ${p}:${String(line)} — inline-tag — ${snippet}\n`);
|
|
208
288
|
}
|
|
209
|
-
process.stderr.write(`\n❌ ${violations.length} legacy-pattern violation(s)\n`);
|
|
210
|
-
return
|
|
289
|
+
process.stderr.write(`\n❌ ${String(violations.length)} legacy-pattern violation(s)\n`);
|
|
290
|
+
return 2;
|
|
211
291
|
}
|
|
212
292
|
if (!QUIET) {
|
|
213
293
|
const scanned = roots.map((r) => _relToPosixOrAbs(r, ROOT)).join(', ');
|
|
@@ -216,39 +296,21 @@ function cmd_scan_dir(root: string): number {
|
|
|
216
296
|
return 0;
|
|
217
297
|
}
|
|
218
298
|
|
|
219
|
-
/** Python repr() of a single string. */
|
|
220
|
-
function _pyRepr(s: string): string {
|
|
221
|
-
const hasSingle = s.includes("'");
|
|
222
|
-
const hasDouble = s.includes('"');
|
|
223
|
-
const quote = hasSingle && !hasDouble ? '"' : "'";
|
|
224
|
-
let out = quote;
|
|
225
|
-
for (const ch of s) {
|
|
226
|
-
if (ch === '\\') out += '\\\\';
|
|
227
|
-
else if (ch === quote) out += '\\' + ch;
|
|
228
|
-
else if (ch === '\n') out += '\\n';
|
|
229
|
-
else if (ch === '\r') out += '\\r';
|
|
230
|
-
else if (ch === '\t') out += '\\t';
|
|
231
|
-
else out += ch;
|
|
232
|
-
}
|
|
233
|
-
return out + quote;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
299
|
interface ParsedArgs {
|
|
237
300
|
stdin: boolean;
|
|
238
301
|
file: string | null;
|
|
239
302
|
scan_dir: string | null;
|
|
240
|
-
strict: boolean;
|
|
241
303
|
verbose: boolean;
|
|
242
304
|
}
|
|
243
305
|
|
|
306
|
+
const USAGE =
|
|
307
|
+
'usage: check_reply_consistency [-h] (--stdin | --file FILE | --scan-dir SCAN_DIR)\n' +
|
|
308
|
+
' [-v] [--quiet]\n';
|
|
309
|
+
|
|
244
310
|
function _usage_error(message: string): never {
|
|
245
|
-
|
|
246
|
-
process.stderr.write(
|
|
247
|
-
'usage: check_reply_consistency [-h] (--stdin | --file FILE | --scan-dir SCAN_DIR)\n' +
|
|
248
|
-
' [--strict] [-v] [--quiet]\n',
|
|
249
|
-
);
|
|
311
|
+
process.stderr.write(USAGE);
|
|
250
312
|
process.stderr.write(`check_reply_consistency: error: ${message}\n`);
|
|
251
|
-
process.exit(
|
|
313
|
+
process.exit(1);
|
|
252
314
|
}
|
|
253
315
|
|
|
254
316
|
function _readStdin(): string {
|
|
@@ -259,11 +321,10 @@ function _readStdin(): string {
|
|
|
259
321
|
}
|
|
260
322
|
}
|
|
261
323
|
|
|
262
|
-
function parse_args(argv: readonly string[]): ParsedArgs {
|
|
324
|
+
export function parse_args(argv: readonly string[]): ParsedArgs {
|
|
263
325
|
let stdin = false;
|
|
264
326
|
let file: string | null = null;
|
|
265
327
|
let scan_dir: string | null = null;
|
|
266
|
-
let strict = false;
|
|
267
328
|
let verbose = false;
|
|
268
329
|
const groupSeen: string[] = [];
|
|
269
330
|
const need = (i: number, name: string): string => {
|
|
@@ -290,17 +351,12 @@ function parse_args(argv: readonly string[]): ParsedArgs {
|
|
|
290
351
|
} else if (arg.startsWith('--scan-dir=')) {
|
|
291
352
|
scan_dir = arg.slice('--scan-dir='.length);
|
|
292
353
|
groupSeen.push('--scan-dir');
|
|
293
|
-
} else if (arg === '--strict') {
|
|
294
|
-
strict = true;
|
|
295
354
|
} else if (arg === '-v' || arg === '--verbose') {
|
|
296
355
|
verbose = true;
|
|
297
356
|
} else if (arg === '--quiet') {
|
|
298
357
|
// handled via module-level QUIET; still a valid flag
|
|
299
358
|
} else if (arg === '-h' || arg === '--help') {
|
|
300
|
-
process.stdout.write(
|
|
301
|
-
'usage: check_reply_consistency [-h] (--stdin | --file FILE | --scan-dir SCAN_DIR)\n' +
|
|
302
|
-
' [--strict] [-v] [--quiet]\n',
|
|
303
|
-
);
|
|
359
|
+
process.stdout.write(USAGE);
|
|
304
360
|
process.exit(0);
|
|
305
361
|
} else {
|
|
306
362
|
_usage_error(`unrecognized arguments: ${arg}`);
|
|
@@ -310,32 +366,37 @@ function parse_args(argv: readonly string[]): ParsedArgs {
|
|
|
310
366
|
_usage_error('one of the arguments --stdin --file --scan-dir is required');
|
|
311
367
|
}
|
|
312
368
|
if (groupSeen.length > 1) {
|
|
313
|
-
|
|
314
|
-
const second = groupSeen[1]!;
|
|
315
|
-
_usage_error(`argument ${second}: not allowed with argument ${first}`);
|
|
369
|
+
_usage_error(`argument ${groupSeen[1]!}: not allowed with argument ${groupSeen[0]!}`);
|
|
316
370
|
}
|
|
317
|
-
return { stdin, file, scan_dir,
|
|
371
|
+
return { stdin, file, scan_dir, verbose };
|
|
318
372
|
}
|
|
319
373
|
|
|
320
|
-
function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
374
|
+
export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
321
375
|
const args = parse_args(argv);
|
|
322
376
|
|
|
323
377
|
if (args.scan_dir !== null) {
|
|
324
378
|
return cmd_scan_dir(args.scan_dir);
|
|
325
379
|
}
|
|
326
380
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
381
|
+
let text: string;
|
|
382
|
+
try {
|
|
383
|
+
text = args.stdin ? _readStdin() : fs.readFileSync(args.file!, 'utf-8');
|
|
384
|
+
} catch (e) {
|
|
385
|
+
process.stderr.write(`error: ${String(e)}\n`);
|
|
386
|
+
return 1;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const findings = check_reply(text);
|
|
390
|
+
if (findings.length === 0) {
|
|
391
|
+
if (args.verbose && !QUIET) {
|
|
392
|
+
process.stdout.write('✅ reply consistent (every options block carries one adjacent recommendation)\n');
|
|
334
393
|
}
|
|
335
394
|
return 0;
|
|
336
395
|
}
|
|
337
|
-
|
|
338
|
-
|
|
396
|
+
for (const f of findings) {
|
|
397
|
+
process.stderr.write(`❌ line ${String(f.line)}: ${f.message}\n`);
|
|
398
|
+
}
|
|
399
|
+
return 2;
|
|
339
400
|
}
|
|
340
401
|
|
|
341
402
|
function _isCliEntry(): boolean {
|
|
@@ -349,8 +410,7 @@ function _isCliEntry(): boolean {
|
|
|
349
410
|
// A symlinked invocation (e.g. via an installed `.augment/` projection,
|
|
350
411
|
// or macOS /var → /private/var temp dirs) makes the raw URLs differ:
|
|
351
412
|
// import.meta.url is the resolved real path while argv[1] keeps the
|
|
352
|
-
// symlink path. Compare realpaths so the entry guard still fires
|
|
353
|
-
// (without this the CLI silently no-ops when run through a symlink).
|
|
413
|
+
// symlink path. Compare realpaths so the entry guard still fires.
|
|
354
414
|
try {
|
|
355
415
|
const here = fs.realpathSync(fileURLToPath(import.meta.url));
|
|
356
416
|
const argv = fs.realpathSync(path.resolve(process.argv[1]));
|
|
@@ -364,15 +424,4 @@ if (_isCliEntry() || process.argv[1] === _HERE) {
|
|
|
364
424
|
process.exit(main());
|
|
365
425
|
}
|
|
366
426
|
|
|
367
|
-
export {
|
|
368
|
-
ROOT,
|
|
369
|
-
OPTION_LINE_RE,
|
|
370
|
-
REC_LINE_RE,
|
|
371
|
-
TAG_RE,
|
|
372
|
-
find_inline_tag,
|
|
373
|
-
find_option_blocks,
|
|
374
|
-
validate,
|
|
375
|
-
cmd_scan_dir,
|
|
376
|
-
parse_args,
|
|
377
|
-
main,
|
|
378
|
-
};
|
|
427
|
+
export { ROOT };
|