@event4u/agent-config 9.25.0 → 9.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +87 -0
- package/README.md +1 -1
- package/dist/agent-src/commands/analyze/conformance.md +2 -2
- package/dist/agent-src/commands/review/changes.md +6 -5
- package/dist/agent-src/commands/roadmap/next.md +194 -0
- package/dist/agent-src/commands/roadmap.md +12 -4
- package/dist/agent-src/contexts/execution/subagent-routing.md +4 -1
- package/dist/agent-src/rules/delegation-policy.md +3 -2
- package/dist/agent-src/rules/design-fidelity.md +17 -1
- package/dist/agent-src/rules/design-review-after-ui-write.md +116 -0
- package/dist/agent-src/rules/evaluator-independence.md +9 -3
- package/dist/agent-src/rules/session-canary.md +2 -0
- package/dist/agent-src/rules/settings-ask-protocol.md +176 -0
- package/dist/agent-src/rules/token-budget-discipline.md +6 -3
- package/dist/agent-src/rules/ui-audit-gate.md +3 -0
- package/dist/agent-src/rules/user-interaction.md +3 -2
- package/dist/agent-src/skills/recursive-verification/SKILL.md +13 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli-delegate/cmd_route_audit.js +4 -1
- package/dist/cli-delegate/cmd_route_explain.js +4 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +94 -23
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +4 -4
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +11 -8
- package/dist/discovery/trust-report.md +3 -3
- package/dist/discovery/workspaces.json +9 -6
- package/dist/hooks/dispatch.js +355 -63
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/router.json +1 -1
- package/dist/server/nicknameResolver.js +116 -0
- package/dist/server/nicknameResolver.js.map +1 -0
- package/dist/server/routes/ping.js +29 -11
- package/dist/server/routes/ping.js.map +1 -1
- package/dist/shared/interactiveContext.js +73 -0
- package/dist/shared/interactiveContext.js.map +1 -0
- package/dist/shared/nicknamePrefill.js +59 -0
- package/dist/shared/nicknamePrefill.js.map +1 -0
- package/dist/shared/settingsAsks.js +128 -0
- package/dist/shared/settingsAsks.js.map +1 -0
- package/dist/shared/settingsConsent.js +93 -0
- package/dist/shared/settingsConsent.js.map +1 -0
- package/package.json +1 -1
- package/src/config/agents-paths.yml +15 -0
- package/src/config/gate-coverage.yml +29 -1
- package/src/config/gate-violation-baselines.json +13 -7
- package/src/scripts/_cli/cmd_route_audit.ts +4 -1
- package/src/scripts/_cli/cmd_route_explain.ts +9 -1
- package/src/scripts/ai-video/stitch.sh +27 -9
- package/src/scripts/before_complete_hook.ts +23 -4
- package/src/scripts/bench_ab_v2_run.ts +272 -16
- package/src/scripts/bench_ab_v2_stats.ts +156 -3
- package/src/scripts/check_completion_review.ts +80 -3
- package/src/scripts/check_corpus_staleness.ts +692 -0
- package/src/scripts/check_gate_coverage.ts +67 -0
- package/src/scripts/check_hook_bundle_freshness.ts +183 -0
- package/src/scripts/conformance_scan.ts +47 -17
- package/src/scripts/dispatch_r2_reviewer.ts +31 -2
- package/src/scripts/git_authorization_hook.ts +75 -10
- package/src/scripts/hook_manifest.yaml +19 -0
- package/src/scripts/hooks/block_no_verify.ts +3 -1
- package/src/scripts/hooks/block_unauthorized_git.ts +133 -16
- package/src/scripts/hooks/design_slop_hook.ts +13 -1
- package/src/scripts/hooks/dispatch_hook.ts +97 -2
- package/src/scripts/hooks/evidence_independence.ts +40 -6
- package/src/scripts/language_mirror_hook.ts +200 -11
- package/src/scripts/lint_abstraction_thresholds.ts +41 -8
- package/src/scripts/lint_agents_layout.ts +2 -0
- package/src/scripts/lint_hook_manifest.ts +29 -0
- package/src/scripts/memory_learn_hook.ts +109 -6
- package/src/scripts/pr_url_reminder_hook.ts +17 -2
- package/src/scripts/release.ts +38 -1
- package/src/scripts/schemas/review-findings.schema.json +99 -0
- package/src/shared/interactiveContext.ts +98 -0
- package/src/shared/nicknamePrefill.ts +96 -0
- package/src/shared/settingsAsks.ts +176 -0
- package/src/shared/settingsConsent.ts +136 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What to put in the nickname field before the user types anything.
|
|
3
|
+
*
|
|
4
|
+
* Phase 4 step 2 of `road-to-zero-ceremony-settings`: *"Ask the nickname once,
|
|
5
|
+
* prefilled from git user name then `$USER`, so accepting is one keypress."*
|
|
6
|
+
* `src/rules/settings-ask-protocol.md` states the chain and closes it with a
|
|
7
|
+
* negation — **"Never `$USER` alone."**
|
|
8
|
+
*
|
|
9
|
+
* WHY THIS MODULE EXISTS. The rule shipped that chain; the one surface that
|
|
10
|
+
* actually prefills shipped a different one. `src/server/routes/ping.ts` read
|
|
11
|
+
* `userInfo().username` and nothing else — precisely the `$USER`-alone shape the
|
|
12
|
+
* rule forbids. A rule and the code it describes disagreeing is drift, not a
|
|
13
|
+
* preference, so the chain now has exactly one implementation and both the
|
|
14
|
+
* server route and the tests read it from here.
|
|
15
|
+
*
|
|
16
|
+
* WHY GIT COMES FIRST. `$USER` is a login handle (`mathiasberg`); `git config
|
|
17
|
+
* user.name` is the name the human chose to be called by (`Mathias Berg`). The
|
|
18
|
+
* whole point of the prefill is that accepting costs one keypress, and that only
|
|
19
|
+
* holds when the prefilled value is the one they would have typed. Note the
|
|
20
|
+
* lookup is deliberately NOT `--local`: the merged config includes
|
|
21
|
+
* `~/.gitconfig`, so the wizard still finds a name when it runs outside a repo.
|
|
22
|
+
*
|
|
23
|
+
* Pure: the git lookup is injected, so the resolver is a function of its probe
|
|
24
|
+
* alone and a test never depends on the machine it runs on — the same shape as
|
|
25
|
+
* `interactiveContext.ts` and `settingsConsent.ts`. The Node-side half (running
|
|
26
|
+
* git, falling back to `userInfo()`) lives in `src/server/nicknameResolver.ts`,
|
|
27
|
+
* because `src/shared/**` may not import Node built-ins and the lint enforces it.
|
|
28
|
+
*
|
|
29
|
+
* @see src/server/nicknameResolver.ts — the impure half
|
|
30
|
+
* @see src/rules/settings-ask-protocol.md § Worked example — the nickname
|
|
31
|
+
* @see docs/contracts/settings-classes.md
|
|
32
|
+
*/
|
|
33
|
+
/** Trim, and treat a whitespace-only value as absent. */
|
|
34
|
+
function _clean(value) {
|
|
35
|
+
return (value ?? '').trim();
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The prefill for the nickname ask, following the documented chain.
|
|
39
|
+
*
|
|
40
|
+
* Order is the rule's, and the negation at the end of it is the load-bearing
|
|
41
|
+
* part: `$USER` is a fallback, never the first answer.
|
|
42
|
+
*/
|
|
43
|
+
export function nicknamePrefill(probe) {
|
|
44
|
+
const git = _clean(probe.gitUserName);
|
|
45
|
+
if (git !== '') {
|
|
46
|
+
return { name: git, source: 'git-user-name' };
|
|
47
|
+
}
|
|
48
|
+
const user = _clean(probe.env['USER']);
|
|
49
|
+
if (user !== '') {
|
|
50
|
+
return { name: user, source: 'env-user' };
|
|
51
|
+
}
|
|
52
|
+
// Windows spells it differently; the rule names it explicitly.
|
|
53
|
+
const username = _clean(probe.env['USERNAME']);
|
|
54
|
+
if (username !== '') {
|
|
55
|
+
return { name: username, source: 'env-username' };
|
|
56
|
+
}
|
|
57
|
+
return { name: '', source: 'none' };
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=nicknamePrefill.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nicknamePrefill.js","sourceRoot":"","sources":["../../src/shared/nicknamePrefill.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAsCH,yDAAyD;AACzD,SAAS,MAAM,CAAC,KAAyB;IACrC,OAAO,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;AAChC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,KAAoB;IAChD,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;IACtC,IAAI,GAAG,KAAK,EAAE,EAAE,CAAC;QACb,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,EAAE,eAAe,EAAE,CAAC;IAClD,CAAC;IACD,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;IACvC,IAAI,IAAI,KAAK,EAAE,EAAE,CAAC;QACd,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC;IAC9C,CAAC;IACD,+DAA+D;IAC/D,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC;IAC/C,IAAI,QAAQ,KAAK,EAAE,EAAE,CAAC;QAClB,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC;IACtD,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;AACxC,CAAC"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one-question budget, as a function rather than as a promise.
|
|
3
|
+
*
|
|
4
|
+
* Phase 5 of `road-to-zero-ceremony-settings`. The rule
|
|
5
|
+
* `src/rules/settings-ask-protocol.md` states the budget — at most ONE settings
|
|
6
|
+
* question per command execution, every further undecided key taking its
|
|
7
|
+
* conservative default silently and being named in the end-summary. Prose is
|
|
8
|
+
* where that obligation has to live, because no gate can count the questions in
|
|
9
|
+
* a chat turn.
|
|
10
|
+
*
|
|
11
|
+
* But the *split* is not a judgement call: given the keys an execution needs
|
|
12
|
+
* decided, which one is asked and what the others resolve to is fully
|
|
13
|
+
* determined. Leaving that determined part in prose too would mean the roadmap's
|
|
14
|
+
* own exit criterion — "a planted fixture needing three B decisions asks once,
|
|
15
|
+
* assumes two conservatively, and lists both in the summary" — had nothing to
|
|
16
|
+
* run against. So the split is computed here and the rule points at it.
|
|
17
|
+
*
|
|
18
|
+
* What this module does NOT do, stated so nobody mistakes it for the fence:
|
|
19
|
+
* it does not ask, it does not write, and it cannot stop an agent that ignores
|
|
20
|
+
* it. The write fence is one layer down and is real —
|
|
21
|
+
* `src/scripts/_cli/cmd_settings_set.ts` and the Fastify settings route both
|
|
22
|
+
* refuse every C-class key and fail closed on an unreadable class contract.
|
|
23
|
+
*
|
|
24
|
+
* Pure — no I/O, no clock, no environment.
|
|
25
|
+
*
|
|
26
|
+
* @see src/rules/settings-ask-protocol.md
|
|
27
|
+
* @see docs/contracts/settings-classes.md
|
|
28
|
+
*/
|
|
29
|
+
import { classOfPath, isConservativeDefault } from './settingsClasses.js';
|
|
30
|
+
/**
|
|
31
|
+
* The end-summary line for a silently defaulted key.
|
|
32
|
+
*
|
|
33
|
+
* `--source manual` and not `jit-answer`: the user did not answer a question, so
|
|
34
|
+
* stamping the write as a just-in-time answer would record a consent nobody
|
|
35
|
+
* gave. The provenance vocabulary exists to keep exactly that distinction.
|
|
36
|
+
*/
|
|
37
|
+
export function changeCommand(key) {
|
|
38
|
+
return `agent-config settings:set ${key} <value> --source manual`;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Plan one execution's settings asks.
|
|
42
|
+
*
|
|
43
|
+
* `needed` is the keys this execution genuinely cannot proceed without, in the
|
|
44
|
+
* order they became needed — the caller's own ordering is respected because the
|
|
45
|
+
* first key to actually block the run is the one that earned the question.
|
|
46
|
+
*
|
|
47
|
+
* `decided` is every key already settled on any resolution layer. A decided key
|
|
48
|
+
* is not a candidate: re-asking a settled question is the cheap question the
|
|
49
|
+
* ask-discipline rules forbid, and it is the single most likely way this budget
|
|
50
|
+
* gets spent on nothing.
|
|
51
|
+
*
|
|
52
|
+
* `defaultOf` supplies the template default. It returns `undefined` for a key
|
|
53
|
+
* the template does not carry, which is treated as unclassified rather than as
|
|
54
|
+
* a default of `undefined` — a missing template entry is a defect, not a value.
|
|
55
|
+
*/
|
|
56
|
+
export function planSettingsAsks(needed, classes, defaultOf, decided = new Set(), options = {}) {
|
|
57
|
+
const plan = { ask: null, silent: [], skipped: [] };
|
|
58
|
+
const seen = new Set();
|
|
59
|
+
// Non-TTY / CI / headless: nobody can answer, so the budget is ZERO rather
|
|
60
|
+
// than one. Every candidate takes its conservative default and the plan
|
|
61
|
+
// carries no ask — "no questions, ever", as a computed fact rather than as a
|
|
62
|
+
// promise the caller has to remember. Nothing is written either: this
|
|
63
|
+
// function never persists, and with no ask there is no `jit-answer` write.
|
|
64
|
+
const interactive = options.interactive ?? true;
|
|
65
|
+
for (const key of needed) {
|
|
66
|
+
// A duplicate in the caller's list must not consume the budget twice.
|
|
67
|
+
if (seen.has(key)) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
seen.add(key);
|
|
71
|
+
if (decided.has(key)) {
|
|
72
|
+
plan.skipped.push({ key, reason: 'already-decided' });
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const cls = classOfPath(classes, key);
|
|
76
|
+
if (cls === undefined) {
|
|
77
|
+
// Unclassified is guarded everywhere else in this system; it is
|
|
78
|
+
// guarded here too. Never fall back to "probably fine".
|
|
79
|
+
plan.skipped.push({ key, reason: 'unclassified' });
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (cls === 'A') {
|
|
83
|
+
plan.skipped.push({ key, reason: 'class-a-never-asked' });
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (cls === 'C') {
|
|
87
|
+
// A C key set to `ask` still produces a RUNTIME question, but that
|
|
88
|
+
// answer lasts one run and is never persisted, so it is not this
|
|
89
|
+
// budget's business. Persisting it stays on the human path.
|
|
90
|
+
plan.skipped.push({ key, reason: 'class-c-guarded' });
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const fallback = defaultOf(key);
|
|
94
|
+
if (fallback === undefined) {
|
|
95
|
+
plan.skipped.push({ key, reason: 'unclassified' });
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (interactive && plan.ask === null) {
|
|
99
|
+
plan.ask = key;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (!isConservativeDefault(fallback)) {
|
|
103
|
+
// Taking a permissive value silently would be a decision made in
|
|
104
|
+
// the user's name — the one outcome the Iron Law names outright.
|
|
105
|
+
// Surfacing it as skipped forces the caller to deal with it instead
|
|
106
|
+
// of inheriting a permission nobody granted.
|
|
107
|
+
plan.skipped.push({ key, reason: 'non-conservative-default' });
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
plan.silent.push({ key, value: fallback, command: changeCommand(key) });
|
|
111
|
+
}
|
|
112
|
+
return plan;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* The end-summary block for a plan, or `null` when nothing was defaulted.
|
|
116
|
+
*
|
|
117
|
+
* `null` rather than an empty string so a caller cannot append a heading with no
|
|
118
|
+
* rows under it. This text rides INSIDE the single end-summary that
|
|
119
|
+
* `direct-answers` already requires — it is never a second summary.
|
|
120
|
+
*/
|
|
121
|
+
export function silentDefaultsSummary(plan) {
|
|
122
|
+
if (plan.silent.length === 0) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
const lines = plan.silent.map((s) => `- \`${s.key}\` → ${JSON.stringify(s.value)} (default) · change: \`${s.command}\``);
|
|
126
|
+
return `Settings resolved without asking this run:\n${lines.join('\n')}`;
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=settingsAsks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settingsAsks.js","sourceRoot":"","sources":["../../src/shared/settingsAsks.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Is a permissive B-class value a RECORDED CONSENT, or just a value?
|
|
3
|
+
*
|
|
4
|
+
* Phase 5 of `road-to-zero-ceremony-settings`, and the answer to that
|
|
5
|
+
* roadmap's Risk 3 — *"the provenance stamp becomes decoration; an unread stamp
|
|
6
|
+
* is cost with no return"*. Before this module the sidecar
|
|
7
|
+
* `settings/.agent-settings.provenance.json` was written by `settings:set` and
|
|
8
|
+
* read by exactly one caller, the GUI, to **display** it. Nothing decided
|
|
9
|
+
* anything on it. This is the reader that makes it load-bearing.
|
|
10
|
+
*
|
|
11
|
+
* The distinction it draws, which a bare value cannot: `true` that a human chose
|
|
12
|
+
* and `true` that the machine inferred are the same byte and are not the same
|
|
13
|
+
* permission. `auto-detected` is the provenance a machine writes about itself —
|
|
14
|
+
* a locale, an installed binary, a detected host. Letting it grant a consent
|
|
15
|
+
* would mean the agent could arrive at its own permission by observing the
|
|
16
|
+
* world, which is the confused-deputy shape in miniature.
|
|
17
|
+
*
|
|
18
|
+
* Modelled on `src/scripts/hooks/block_unauthorized_git.ts`: answer from a
|
|
19
|
+
* recorded fact rather than from the model's recollection, and treat a missing
|
|
20
|
+
* record as NOT granted rather than as probably-fine.
|
|
21
|
+
*
|
|
22
|
+
* WHAT THIS IS NOT. It is not a hook and it does not intercept anything. No
|
|
23
|
+
* B-class key currently gates an action through a guard — the two that gate
|
|
24
|
+
* behaviour at all (`personal.open_edited_files`, `memory.learn_on_session_end`)
|
|
25
|
+
* are read as values by the installer, the wizard and one session_end concern,
|
|
26
|
+
* and the *action* they govern is prose. So the enforcement gradient is honest
|
|
27
|
+
* and steep: this function is deterministic, the obligation to call it is
|
|
28
|
+
* model-carried, and on a prose-only host ask-once can degrade to ask-never.
|
|
29
|
+
*
|
|
30
|
+
* Pure — no I/O. Callers supply the value and the sidecar entry.
|
|
31
|
+
*
|
|
32
|
+
* @see src/rules/settings-ask-protocol.md
|
|
33
|
+
* @see docs/contracts/settings-classes.md
|
|
34
|
+
*/
|
|
35
|
+
import { isConservativeDefault } from './settingsClasses.js';
|
|
36
|
+
/**
|
|
37
|
+
* The sources that can carry a human decision.
|
|
38
|
+
*
|
|
39
|
+
* `jit-answer` is a question the user answered. `gui` is a human at the settings
|
|
40
|
+
* editor. `manual` is a hand-edit or an explicit CLI write on the user's
|
|
41
|
+
* instruction. `auto-detected` is excluded on purpose — see the module header.
|
|
42
|
+
*/
|
|
43
|
+
const HUMAN_SOURCES = ['jit-answer', 'gui', 'manual'];
|
|
44
|
+
/**
|
|
45
|
+
* The verdict for one consent-gated action.
|
|
46
|
+
*
|
|
47
|
+
* Order matters: class first (a C key never reaches a consent question at all),
|
|
48
|
+
* then the value (conservative means no, regardless of provenance), then the
|
|
49
|
+
* record.
|
|
50
|
+
*/
|
|
51
|
+
export function consentVerdict(query) {
|
|
52
|
+
if (query.cls !== 'B') {
|
|
53
|
+
return 'not-a-consent-key';
|
|
54
|
+
}
|
|
55
|
+
if (isConservativeDefault(query.value)) {
|
|
56
|
+
return 'withheld-default';
|
|
57
|
+
}
|
|
58
|
+
if (query.handEdited === true) {
|
|
59
|
+
return 'granted';
|
|
60
|
+
}
|
|
61
|
+
if (query.source === undefined) {
|
|
62
|
+
return 'withheld-unrecorded';
|
|
63
|
+
}
|
|
64
|
+
if (!HUMAN_SOURCES.includes(query.source)) {
|
|
65
|
+
return 'withheld-machine-inferred';
|
|
66
|
+
}
|
|
67
|
+
return 'granted';
|
|
68
|
+
}
|
|
69
|
+
/** `true` only for `granted` — every other verdict withholds. */
|
|
70
|
+
export function consentGranted(query) {
|
|
71
|
+
return consentVerdict(query) === 'granted';
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* One line explaining a withheld verdict, for the surface that refused.
|
|
75
|
+
*
|
|
76
|
+
* `null` for `granted`, so a caller cannot accidentally print a refusal reason
|
|
77
|
+
* on the allow path.
|
|
78
|
+
*/
|
|
79
|
+
export function withheldReason(verdict, key) {
|
|
80
|
+
switch (verdict) {
|
|
81
|
+
case 'granted':
|
|
82
|
+
return null;
|
|
83
|
+
case 'withheld-default':
|
|
84
|
+
return `${key} is at its conservative default — absent and no are the same answer.`;
|
|
85
|
+
case 'withheld-unrecorded':
|
|
86
|
+
return `${key} permits this, but nothing records who decided it. Ask, or set it via the GUI.`;
|
|
87
|
+
case 'withheld-machine-inferred':
|
|
88
|
+
return `${key} was auto-detected, not decided. A machine inference is not a consent.`;
|
|
89
|
+
case 'not-a-consent-key':
|
|
90
|
+
return `${key} is not a class-B consent key — this gate does not apply to it.`;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=settingsConsent.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settingsConsent.js","sourceRoot":"","sources":["../../src/shared/settingsConsent.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,EAAE,qBAAqB,EAAsB,MAAM,sBAAsB,CAAC;AAKjF;;;;;;GAMG;AACH,MAAM,aAAa,GAA6B,CAAC,YAAY,EAAE,KAAK,EAAE,QAAQ,CAAC,CAAC;AAsChF;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAAC,KAAmB;IAC9C,IAAI,KAAK,CAAC,GAAG,KAAK,GAAG,EAAE,CAAC;QACpB,OAAO,mBAAmB,CAAC;IAC/B,CAAC;IACD,IAAI,qBAAqB,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;QACrC,OAAO,kBAAkB,CAAC;IAC9B,CAAC;IACD,IAAI,KAAK,CAAC,UAAU,KAAK,IAAI,EAAE,CAAC;QAC5B,OAAO,SAAS,CAAC;IACrB,CAAC;IACD,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QAC7B,OAAO,qBAAqB,CAAC;IACjC,CAAC;IACD,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC;QACxC,OAAO,2BAA2B,CAAC;IACvC,CAAC;IACD,OAAO,SAAS,CAAC;AACrB,CAAC;AAED,iEAAiE;AACjE,MAAM,UAAU,cAAc,CAAC,KAAmB;IAC9C,OAAO,cAAc,CAAC,KAAK,CAAC,KAAK,SAAS,CAAC;AAC/C,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,OAAuB,EAAE,GAAW;IAC/D,QAAQ,OAAO,EAAE,CAAC;QACd,KAAK,SAAS;YACV,OAAO,IAAI,CAAC;QAChB,KAAK,kBAAkB;YACnB,OAAO,GAAG,GAAG,sEAAsE,CAAC;QACxF,KAAK,qBAAqB;YACtB,OAAO,GAAG,GAAG,gFAAgF,CAAC;QAClG,KAAK,2BAA2B;YAC5B,OAAO,GAAG,GAAG,wEAAwE,CAAC;QAC1F,KAAK,mBAAmB;YACpB,OAAO,GAAG,GAAG,iEAAiE,CAAC;IACvF,CAAC;AACL,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@event4u/agent-config",
|
|
3
3
|
"mcpName": "io.github.event4u-app/agent-config",
|
|
4
|
-
"version": "9.
|
|
4
|
+
"version": "9.26.0",
|
|
5
5
|
"description": "Governed skills, rules and commands for AI coding tools (Claude Code, Cursor, Windsurf, Copilot) — every claim machine-checked, including \"zero runtime daemon\".",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"private": false,
|
|
@@ -114,6 +114,21 @@
|
|
|
114
114
|
writer: /team-knowledge:bootstrap skill
|
|
115
115
|
rationale: Deterministic template pages; review-then-commit flow (nothing here is the artifact)
|
|
116
116
|
|
|
117
|
+
# ---------------------------------------------------------------------------
|
|
118
|
+
# Self-improvement proposals (TRACKED — listed for classification, not ignored)
|
|
119
|
+
# ---------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
- path: /agents/proposals/
|
|
122
|
+
scope: both
|
|
123
|
+
policy: tracked
|
|
124
|
+
writer: learning-to-rule-or-skill skill (drafts) → upstream-contribute (PR)
|
|
125
|
+
rationale: |
|
|
126
|
+
Proposal drafts must be committed — the proposal-drift workflow reads their
|
|
127
|
+
stage: frontmatter in CI, and upstream-contribute opens a PR from them. Listed
|
|
128
|
+
here because agents-layout.md requires every new top-level agents/ entry to
|
|
129
|
+
appear in this manifest; the entry is inert for the gitignore gate, which reads
|
|
130
|
+
policy: ignored only.
|
|
131
|
+
|
|
117
132
|
# ---------------------------------------------------------------------------
|
|
118
133
|
# Ghostwriter profiles
|
|
119
134
|
# ---------------------------------------------------------------------------
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
# ----------------------------------------------------------
|
|
45
45
|
# 240 `lint_*` / `check_*` / `audit_*` scripts live under `src/scripts/`
|
|
46
46
|
# (the count is computed, not asserted — `check_gate_coverage.count_gate_scripts`).
|
|
47
|
-
# **
|
|
47
|
+
# **32 of them emit the machine-readable `scanned: <N>` line under their
|
|
48
48
|
# CI-identical argv, and all 31 are listed below.** The manifest is therefore
|
|
49
49
|
# complete with respect to its own contract, and covers ~13 % of the population.
|
|
50
50
|
#
|
|
@@ -599,3 +599,31 @@ gates:
|
|
|
599
599
|
check_kernel_prefix_stability). The canary is what distinguishes that from
|
|
600
600
|
a gate scanning nothing — it plants the removal-permitting class with no
|
|
601
601
|
evidence and the gate reds.
|
|
602
|
+
|
|
603
|
+
- id: check_corpus_staleness
|
|
604
|
+
argv: ["--quiet", "--today", "2026-08-06"]
|
|
605
|
+
min_scanned: 5
|
|
606
|
+
corpus: "src/skills/*/data/manifest.json — grounding-corpus manifests (6 at baseline; brand, database, accessibility-auditor, api-design, design-intelligence, threat-modeling)"
|
|
607
|
+
status: enforced
|
|
608
|
+
canary:
|
|
609
|
+
class: missing-declared-csv
|
|
610
|
+
path: src/skills/zz-canary-corpus/data/manifest.json
|
|
611
|
+
content: |
|
|
612
|
+
{
|
|
613
|
+
"manifest_version": 1,
|
|
614
|
+
"refresh_cadence": "quarterly",
|
|
615
|
+
"upstream": { "last_checked": "2026-07-01" },
|
|
616
|
+
"domains": { "d": { "file": "nope.csv", "search_cols": ["a"], "output_cols": ["b"] } }
|
|
617
|
+
}
|
|
618
|
+
note: >
|
|
619
|
+
`--today` is PINNED in argv on purpose, and it is the whole reason this
|
|
620
|
+
gate is testable at all: the five upstream-vendored corpora share
|
|
621
|
+
`last_checked: 2026-06-07`, so an unpinned reference date makes this row
|
|
622
|
+
flip from green to red on 2026-09-15 with no diff behind it. The pinned
|
|
623
|
+
date keeps the coverage guard measuring COVERAGE; the real calendar check
|
|
624
|
+
is the workflow step, which runs unpinned and is meant to red when the
|
|
625
|
+
cadence is actually missed. Changing the pin is how you re-baseline after a
|
|
626
|
+
re-vendor, and it should be visible in a diff.
|
|
627
|
+
The canary plants a manifest whose declared CSV does not exist — the exact
|
|
628
|
+
gap `corpus-grounding/scripts/schema_validator.ts` leaves open, since it
|
|
629
|
+
computes the path and never opens the file.
|
|
@@ -1,36 +1,42 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$comment": "Violation ratchet for gates whose dead scan root was repaired. A repair REVEALS pre-existing debt; it does not create it. Each entry records the count revealed at repair time
|
|
2
|
+
"$comment": "Violation ratchet for gates whose dead scan root was repaired. A repair REVEALS pre-existing debt; it does not create it. Each entry records the count revealed at repair time — the gate fails only when its count RISES. Lowering a count is a normal commit; raising one is a defect. Entries expire: a baseline that has not dropped within 56 days of `landed` (or of `reaffirmed.date`) fails its gate, because a number that never moves is suppression with extra steps. Mechanism + rationale: src/scripts/_lib/gate_baseline.ts. Decision: AI council 2026-08-02, roadmap road-to-gates-that-can-fail blocker `dead-gate-finding-triage`.",
|
|
3
3
|
"gates": {
|
|
4
4
|
"check_no_new_legacy_path:hardcoded-scan-roots": {
|
|
5
5
|
"count": 49,
|
|
6
6
|
"landed": "2026-08-04",
|
|
7
|
-
"note": "Lines under src/scripts/**/*.{ts,mts,mjs} that pass the retired `.agent-src.uncondensed` directory into `path.join(...)`
|
|
7
|
+
"note": "Lines under src/scripts/**/*.{ts,mts,mjs} that pass the retired `.agent-src.uncondensed` directory into `path.join(...)` — a filesystem path CONSTRUCTED against the tree ADR-051 abandoned, which is the exact shape of all 14 dead gates. Deliberately narrow so the number is signal, not noise: 236 raw mentions under src/scripts → 147 string-literal lines → 67 path-constructing lines when the pass was written, after exempting src/scripts/_lib/agent_src.ts (the shared resolver, whose job IS owning the constant — gates that route through it survived the move). Detectors holding the literal as data to match, strip, or print are not counted, and no prose, docs, tests or fixtures are read: that full-tree shape was measured at 44 files / 213 hits and rejected as the wrong lint (see the note in check_no_new_legacy_path.ts::find_offenders). Recorded at 58 rather than 67 because Phase 1's scan-root repairs were landing on this branch while the pass was being written and had already removed 9 — a baseline above the truth silently permits new violations. This count goes to ZERO as the remaining repairs land; every repair is a lowering commit. Lowered 56→51 by the road-to-renewal-foundation Phase 1 dead-root sweep (check_always_budget, check_portability, check_references, lint_framework_leakage, lint_agents_md). Lowered 51→49 by the gate-hardening adoption PR: skill_collision_clusters and skill_overlap were repointed at the live skills tree via the shared resolver — two gates that had been reading the retired container and scoring 0 of 288 skills."
|
|
8
8
|
},
|
|
9
9
|
"audit_user_type_axis": {
|
|
10
10
|
"count": 1,
|
|
11
11
|
"landed": "2026-08-02",
|
|
12
|
-
"note": "One orphan user-type value: `legal`. Five legal-review-prep skills (contract-review, dpa-review, nda-triage, legal-intake-triage, legal-practice-profile) declare `recommended_for_user_types: [legal]`, but there is no `user-types/legal.yml`
|
|
12
|
+
"note": "One orphan user-type value: `legal`. Five legal-review-prep skills (contract-review, dpa-review, nda-triage, legal-intake-triage, legal-practice-profile) declare `recommended_for_user_types: [legal]`, but there is no `user-types/legal.yml` — the pack shipped an axis value with no user-type definition behind it, and the dead scan root hid it (the audit reported used=0 orphans=0 while reading zero skills; it now reads 288 and reports used=8). Clearing this entry is a product call, not a copy edit: either author `user-types/legal.yml` (tagline, recommended packs, install-path hint) or drop `legal` from those five skills. Not decided here because both options change shipped pack behaviour."
|
|
13
13
|
},
|
|
14
14
|
"lint_handoffs": {
|
|
15
15
|
"count": 18,
|
|
16
16
|
"landed": "2026-08-02",
|
|
17
|
-
"note": "All 18 are `handoff_tier_mismatch`: a senior-tier skill links to a peer whose frontmatter carries no `tier` (reported as tier='unset')
|
|
17
|
+
"note": "All 18 are `handoff_tier_mismatch`: a senior-tier skill links to a peer whose frontmatter carries no `tier` (reported as tier='unset') — e.g. build-buy-partner → adr-create, customer-research → po-discovery, gated-reach → threat-modeling. This is a tier-metadata backfill backlog on the LINKED-TO skills, not a broken link. The one genuinely dangling link revealed by the same repair (competitive-positioning → a non-existent ../analyze-reference-repo/SKILL.md; it is a command, not a skill) was FIXED in the repairing commit rather than baselined. Clearing this entry means backfilling `tier` on the linked-to skills, or deciding that `tier: unset` on a link target is not a violation and narrowing the rule."
|
|
18
18
|
},
|
|
19
19
|
"lint_pack_boundaries": {
|
|
20
20
|
"count": 337,
|
|
21
21
|
"landed": "2026-08-02",
|
|
22
|
-
"note": "Markdown links between source artefacts that cross a pack boundary the source pack's `requires` graph does not reach
|
|
22
|
+
"note": "Markdown links between source artefacts that cross a pack boundary the source pack's `requires` graph does not reach — revealed when the scan root moved off the deleted packages/ tree to the live corpus (0 -> 753 artefacts scanned). 82 distinct pack pairs across 131 source files; largest single class is engineering-base -> meta (73), skills in a consumer-installable pack citing this package's own maintenance skills. Each is a link that dangles for a consumer who installs the source pack without the target's. Judged real drift rather than an over-strict gate because it is long-tailed — top bucket 22%, no single systematic cause — unlike the lint_artefact_frontmatter case, where one required shape had 0-of-288 adoption. Fix per finding: add the edge to packs.yml requires, retarget the link, or move the artefact."
|
|
23
23
|
},
|
|
24
24
|
"check_gate_completeness": {
|
|
25
25
|
"count": 217,
|
|
26
26
|
"landed": "2026-08-05",
|
|
27
|
-
"note": "Ledger adoption ratchet. 217 of 221 CI-registered gates do not yet carry per-target completeness accounting (`_lib/gate_ledger.ts`) or a `// ledger-exempt:` marker. This is deliberately NOT a sweep: road-to-skill-ecosystem-gate-integrity Phase 1 adopts exactly three gates (lint_framework_leakage, check_references, lint_roadmap_complexity) plus the checker itself to prove the interface, and the remainder migrates as gates are touched. Shrink-only from here
|
|
27
|
+
"note": "Ledger adoption ratchet. 217 of 221 CI-registered gates do not yet carry per-target completeness accounting (`_lib/gate_ledger.ts`) or a `// ledger-exempt:` marker. This is deliberately NOT a sweep: road-to-skill-ecosystem-gate-integrity Phase 1 adopts exactly three gates (lint_framework_leakage, check_references, lint_roadmap_complexity) plus the checker itself to prove the interface, and the remainder migrates as gates are touched. Shrink-only from here — the number may not grow, so a NEW gate must adopt or exempt.",
|
|
28
28
|
"falsifier": "./scripts-run src/scripts/check_gate_completeness"
|
|
29
29
|
},
|
|
30
30
|
"check_depth_budget": {
|
|
31
31
|
"count": 4,
|
|
32
32
|
"landed": "2026-08-06",
|
|
33
|
-
"note": "Four depth files exceed the 16,000-char per-file ceiling: rule-body-migration-inventory.md (23,782), design-antipatterns.md (20,707), roadmap-process-loop.md (20,360), orchestration-telemetry.md (16,155). Recorded rather than fixed because splitting four large reference documents is unrelated to the change that introduced the ceiling, and a gate that lands as four instant blockers is the shape this repository has already recorded as a failure. Shrink-only from here: a FIFTH over-ceiling file reds. roadmap-process-loop.md is the file that motivated the gate
|
|
33
|
+
"note": "Four depth files exceed the 16,000-char per-file ceiling: rule-body-migration-inventory.md (23,782), design-antipatterns.md (20,707), roadmap-process-loop.md (20,360), orchestration-telemetry.md (16,155). Recorded rather than fixed because splitting four large reference documents is unrelated to the change that introduced the ceiling, and a gate that lands as four instant blockers is the shape this repository has already recorded as a failure. Shrink-only from here: a FIFTH over-ceiling file reds. roadmap-process-loop.md is the file that motivated the gate — it states 'Size budget: <= 4,000 chars' in its own header and is 5x over it, which is what a declared budget with nothing measuring it decays into. The ceiling is a GROWTH RATCHET, not a measured quality threshold: the 7,308-trajectory study behind the 3,500-token skill ceiling (ADR-217) was taken on procedural always-co-loaded skills, and this layer is declarative and loaded one or two files at a time. Replace the number when someone measures comprehension on THIS layer."
|
|
34
|
+
},
|
|
35
|
+
"gate-self-test:registered-non-adopters": {
|
|
36
|
+
"count": 24,
|
|
37
|
+
"landed": "2026-08-06",
|
|
38
|
+
"note": "Self-test adoption ratchet, expressed as a shrink-only NON-adopter count. 24 of the 32 gates registered enforced in gate-coverage.yml carry neither a `--self-test` (`_lib/gate_self_test.ts`) nor a `// self-test-exempt: <reason>` marker, so nothing proves they DISCRIMINATE — an enforced `scanned:` floor proves a gate read something, only a self-test proves the reading changes the verdict. Deliberately NOT an adoption column ratcheted upward: check_gate_coverage.ts's own report_hardening_ratchet rejects that shape in this same file (\"grades the solution instead of the problem\"), and the sibling adoption ratchet (check_gate_completeness) is already a non-adopter count. Population is the manifest, not every script under src/scripts: padding the manifest to move this number would mean adding a floor per row. Shrink-only from here — a NEW registered gate must adopt or exempt. Migration mechanism, same as the ledger ratchet above: gates adopt as they are touched, not in a sweep — the eight current adopters were each added alongside a real change to that gate. The 56-day non-stagnation clause applies unchanged, so a count parked here fails on its own.",
|
|
39
|
+
"falsifier": "./scripts-run src/scripts/check_gate_coverage"
|
|
34
40
|
}
|
|
35
41
|
}
|
|
36
42
|
}
|
|
@@ -36,8 +36,11 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
36
36
|
import { match_prompt, type Router } from '../_lib/router_match.js';
|
|
37
37
|
import { load_agent_settings } from '../_lib/agent_settings.js';
|
|
38
38
|
import { read_entries } from '../chat_history.js';
|
|
39
|
+
import { resolvePackageRoot } from '../_lib/package_root.js';
|
|
39
40
|
|
|
40
|
-
|
|
41
|
+
// Marker-anchored, not hop-counted — see the sibling note in cmd_route_explain.ts.
|
|
42
|
+
// Both routing commands were the last two `_cli` entries still counting hops.
|
|
43
|
+
const REPO_ROOT = resolvePackageRoot(import.meta.url);
|
|
41
44
|
const ROUTER_JSON = path.join(REPO_ROOT, 'dist', 'router.json');
|
|
42
45
|
|
|
43
46
|
// cache-version: v1 — rebuildable audit log: delete-and-rerun rebuilds it from
|
|
@@ -31,8 +31,16 @@ import {
|
|
|
31
31
|
} from '../_lib/router_match.js';
|
|
32
32
|
import { measure, method_note } from '../_lib/token_count.js';
|
|
33
33
|
import { load_agent_settings } from '../_lib/agent_settings.js';
|
|
34
|
+
import { resolvePackageRoot } from '../_lib/package_root.js';
|
|
34
35
|
|
|
35
|
-
|
|
36
|
+
// Marker-anchored, not hop-counted: this module runs from `src/scripts/_cli/`
|
|
37
|
+
// (three levels down) AND from the precompiled `dist/cli-delegate/` (two), and
|
|
38
|
+
// a fixed hop count is right in exactly one of them. Three hops from the bundle
|
|
39
|
+
// landed on `<pkg>/..` — `node_modules/@event4u` for a scoped install — so
|
|
40
|
+
// `route:explain` and `route:audit` reported dist/router.json as missing on
|
|
41
|
+
// every invocation through the shipped binary while both worked under `tsx`.
|
|
42
|
+
// The resolver exists precisely for this and 12 sibling commands already use it.
|
|
43
|
+
const REPO_ROOT = resolvePackageRoot(import.meta.url);
|
|
36
44
|
const ROUTER_JSON = path.join(REPO_ROOT, 'dist', 'router.json');
|
|
37
45
|
const RULES_DIST_DIR = path.join(REPO_ROOT, 'dist', 'agent-src', 'rules');
|
|
38
46
|
|
|
@@ -10,7 +10,10 @@
|
|
|
10
10
|
# stitch.sh <manifest.json> <output.mp4>
|
|
11
11
|
# [--skip-scene <id> ...]
|
|
12
12
|
# [--abort-on-missing | --continue]
|
|
13
|
-
#
|
|
13
|
+
#
|
|
14
|
+
# NOT a flag: --crossfade. It is parsed and REFUSED (exit 2). See
|
|
15
|
+
# "Refused flags" below — it is listed so a caller who read the old
|
|
16
|
+
# usage line gets an error instead of a hard cut.
|
|
14
17
|
#
|
|
15
18
|
# Failure semantics:
|
|
16
19
|
# - Missing clip → fail loud with scene_id and re-render hint
|
|
@@ -21,6 +24,22 @@
|
|
|
21
24
|
# <project>/scenes/<id>/error.json; stitch.sh checks for that
|
|
22
25
|
# file alongside the clip and surfaces it instead of an opaque
|
|
23
26
|
# ffmpeg error.
|
|
27
|
+
#
|
|
28
|
+
# Refused flags:
|
|
29
|
+
# --crossfade <seconds> — accepted by the parser, then REFUSED with
|
|
30
|
+
# exit 2 before any work. It used to print "not yet implemented"
|
|
31
|
+
# to stderr and fall straight through to the plain concat, so the
|
|
32
|
+
# script reported success (exit 0, success JSON) while delivering a
|
|
33
|
+
# hard cut. Worse, the notice sat AFTER the dry-run exit, and
|
|
34
|
+
# AIV_DRYRUN defaults to true — so in the default mode the flag was
|
|
35
|
+
# accepted in total silence.
|
|
36
|
+
# A crossfade is not a small addition here: the output path is
|
|
37
|
+
# `-f concat -c copy`, and stream copy precludes any filtergraph, so
|
|
38
|
+
# xfade/acrossfade need a re-encode path that does not exist. It was
|
|
39
|
+
# also never reachable from a governed surface: /video:stitch passes
|
|
40
|
+
# only --skip-scene and --continue and rejects anything else. So the
|
|
41
|
+
# honest state is a refusal, not a silent downgrade and not a
|
|
42
|
+
# filtergraph shipped into a script with no test harness.
|
|
24
43
|
|
|
25
44
|
set -euo pipefail
|
|
26
45
|
|
|
@@ -39,19 +58,23 @@ esac
|
|
|
39
58
|
MANIFEST="${1:-}"
|
|
40
59
|
OUTPUT="${2:-}"
|
|
41
60
|
[ -n "${MANIFEST}" ] && [ -n "${OUTPUT}" ] \
|
|
42
|
-
|| aiv_die 2 "usage: stitch.sh <manifest.json> <output.mp4> [--skip-scene <id>] [--abort-on-missing|--continue]
|
|
61
|
+
|| aiv_die 2 "usage: stitch.sh <manifest.json> <output.mp4> [--skip-scene <id>] [--abort-on-missing|--continue]"
|
|
43
62
|
[ -r "${MANIFEST}" ] || aiv_die 2 "manifest not readable: ${MANIFEST}"
|
|
44
63
|
shift 2 || true
|
|
45
64
|
|
|
46
65
|
SKIP_IDS=""
|
|
47
66
|
MISSING_POLICY="abort"
|
|
48
|
-
CROSSFADE=""
|
|
49
67
|
while [ "$#" -gt 0 ]; do
|
|
50
68
|
case "$1" in
|
|
51
69
|
--skip-scene) SKIP_IDS="${SKIP_IDS} ${2:-}"; shift 2 ;;
|
|
52
70
|
--abort-on-missing) MISSING_POLICY="abort"; shift ;;
|
|
53
71
|
--continue) MISSING_POLICY="continue"; shift ;;
|
|
54
|
-
--crossfade)
|
|
72
|
+
--crossfade)
|
|
73
|
+
# Refused HERE — before the dry-run branch, before ffmpeg, before any
|
|
74
|
+
# output. The old code warned after the dry-run exit and then concatenated
|
|
75
|
+
# anyway, so a caller asking for a crossfade got a hard cut and exit 0.
|
|
76
|
+
aiv_die 2 "stitch.sh: --crossfade is not implemented and will not be silently downgraded. The output path is 'ffmpeg -f concat -c copy', and stream copy precludes any filtergraph, so xfade/acrossfade require a re-encode path this script does not have. Re-run without --crossfade for a hard-cut concat, or open a roadmap item for the re-encode path."
|
|
77
|
+
;;
|
|
55
78
|
*) aiv_die 2 "stitch.sh: unknown flag '$1'" ;;
|
|
56
79
|
esac
|
|
57
80
|
done
|
|
@@ -149,11 +172,6 @@ if [ "${DRYRUN_FLAG}" -eq 1 ]; then
|
|
|
149
172
|
exit 0
|
|
150
173
|
fi
|
|
151
174
|
|
|
152
|
-
if [ -n "${CROSSFADE}" ]; then
|
|
153
|
-
printf 'stitch: crossfade=%ss requested — passing through ffmpeg xfade filter not yet implemented (concat path used)\n' \
|
|
154
|
-
"${CROSSFADE}" >&2
|
|
155
|
-
fi
|
|
156
|
-
|
|
157
175
|
ffmpeg -loglevel error -y -f concat -safe 0 -i "${CONCAT_LIST}" \
|
|
158
176
|
-c copy "${OUTPUT}" >/dev/null \
|
|
159
177
|
|| aiv_die 8 "ffmpeg concat failed (manifest=${MANIFEST}, output=${OUTPUT})"
|
|
@@ -205,10 +205,17 @@ const _VACUOUS_PATTERNS: readonly RegExp[] = [
|
|
|
205
205
|
export function isVacuousOutput(output: string): boolean {
|
|
206
206
|
const text = output.trim();
|
|
207
207
|
if (!text) {
|
|
208
|
-
//
|
|
209
|
-
|
|
208
|
+
// Silence is the Unix convention for success: a clean `tsc --noEmit`,
|
|
209
|
+
// `eslint`, or `phpstan` prints nothing. Round 2 caught this reading empty
|
|
210
|
+
// output as vacuity, which quietly stopped counting the most common green
|
|
211
|
+
// signal in this repo. Genuinely unreadable CI polls are handled separately
|
|
212
|
+
// (`pendingCount` returns null and the poll counts for nothing).
|
|
213
|
+
return false;
|
|
210
214
|
}
|
|
211
|
-
|
|
215
|
+
// Per RESULT LINE, not per blob: a composite run whose 812 tests passed and
|
|
216
|
+
// whose one empty sub-package printed "No test files found" is evidence.
|
|
217
|
+
const lines = text.split("\n").filter((l) => l.trim());
|
|
218
|
+
return lines.every((l) => _VACUOUS_PATTERNS.some((re) => re.test(l)));
|
|
212
219
|
}
|
|
213
220
|
|
|
214
221
|
/** Commands that poll CI state rather than produce a local result. */
|
|
@@ -228,8 +235,20 @@ export function pendingCount(output: string): number | null {
|
|
|
228
235
|
return Number(m[1]);
|
|
229
236
|
}
|
|
230
237
|
// `gh pr checks` prints one row per check; count the in-progress markers.
|
|
238
|
+
// `gh pr checks` prints one row per check (`name\tpass\t1m2s\t…`). Count the
|
|
239
|
+
// in-flight rows; a table with rows but none pending IS a settle, which the
|
|
240
|
+
// first version could not see because it returned null and never counted.
|
|
231
241
|
const rows = output.match(/^\S.*\b(pending|in_progress|queued)\b/gim);
|
|
232
|
-
|
|
242
|
+
if (rows) {
|
|
243
|
+
return rows.length;
|
|
244
|
+
}
|
|
245
|
+
if (/\bno checks reported\b/i.test(output)) {
|
|
246
|
+
return null; // the push→registration gap — not a settle
|
|
247
|
+
}
|
|
248
|
+
if (/^\S+\s+(pass|fail|skipping|successful|failing)\b/im.test(output)) {
|
|
249
|
+
return 0; // a real result table with no in-flight rows
|
|
250
|
+
}
|
|
251
|
+
return null;
|
|
233
252
|
}
|
|
234
253
|
|
|
235
254
|
/**
|