@event4u/agent-config 10.1.0 → 10.2.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 +109 -0
- package/README.md +1 -1
- package/dist/agent-src/commands/roadmap/next.md +38 -0
- package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +22 -1
- package/dist/agent-src/contexts/contracts/frugality-charter.md +1 -1
- package/dist/agent-src/rules/context-hygiene.md +39 -0
- package/dist/agent-src/rules/token-budget-discipline.md +3 -2
- package/dist/agent-src/rules/token-efficiency.md +22 -0
- package/dist/agent-src/skills/ai-council/SKILL.md +28 -6
- package/dist/agent-src/skills/condense-memory/SKILL.md +1 -1
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +1 -1
- package/dist/agent-src/skills/token-optimizer/SKILL.md +5 -1
- package/dist/agent-src/templates/agent-settings.md +12 -28
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +2 -2
- package/dist/agent-src/templates/scripts/work_engine/_lib/agent_settings.ts +0 -1
- package/dist/cli/registry.js +1 -1
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-3JUTT26I.js → chunk-6ENSXSBW.js} +1 -1
- package/dist/cli-delegate/{chunk-TVGXH7VA.js → chunk-6LOPHJLD.js} +1 -1
- package/dist/cli-delegate/{chunk-XT2YTJCZ.js → chunk-AYKQGIBG.js} +4 -0
- package/dist/cli-delegate/{chunk-HJSKTMIF.js → chunk-BV3WR67D.js} +3 -19
- package/dist/cli-delegate/{chunk-HTIU3W56.js → chunk-BWMXLD4B.js} +6 -2
- package/dist/cli-delegate/{chunk-2H5SE7DU.js → chunk-E4RUZKXB.js} +165 -38
- package/dist/cli-delegate/{chunk-XTC5ARB6.js → chunk-IR3IVRVC.js} +7 -2
- package/dist/cli-delegate/{chunk-LRXJK6GN.js → chunk-JMQZ7MM5.js} +1 -1
- package/dist/cli-delegate/{chunk-NX5AXSLD.js → chunk-X3VOARZD.js} +3 -3
- package/dist/cli-delegate/cmd_analyze_session.js +1 -1
- package/dist/cli-delegate/cmd_brand_status.js +1 -1
- package/dist/cli-delegate/cmd_conformance.js +881 -204
- package/dist/cli-delegate/cmd_doctor.js +3 -3
- package/dist/cli-delegate/cmd_explain.js +2 -2
- package/dist/cli-delegate/cmd_export.js +4 -4
- package/dist/cli-delegate/cmd_handoff.js +6 -5
- package/dist/cli-delegate/cmd_mcp_available.js +1 -1
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_packs_active.js +2 -2
- package/dist/cli-delegate/cmd_prune.js +4 -4
- package/dist/cli-delegate/cmd_route_audit.js +2 -2
- package/dist/cli-delegate/cmd_route_explain.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +44 -9
- package/dist/cli-delegate/cmd_settings_get.js +2 -2
- package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
- package/dist/cli-delegate/cmd_settings_set.js +2 -2
- 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 +1 -1
- package/dist/cli-delegate/cmd_upgrade.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +4 -4
- package/dist/cli-delegate/cmd_versions.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +11 -11
- 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 +88 -20
- package/dist/install/install.mjs +9 -21
- package/dist/mcp/registry-manifest.json +1 -1
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1336 -489
- package/dist/scripts/_lib/agent_settings.js +25 -2
- package/dist/scripts/_lib/agent_settings.js.map +1 -1
- package/dist/scripts/_lib/compile_time_toggles.js +4 -3
- package/dist/scripts/_lib/compile_time_toggles.js.map +1 -1
- package/dist/server/io/yamlIO.js +0 -2
- package/dist/server/io/yamlIO.js.map +1 -1
- package/dist/server/schemas/settings.js +3 -9
- package/dist/server/schemas/settings.js.map +1 -1
- package/dist/shared/settingsClasses.js +64 -2
- package/dist/shared/settingsClasses.js.map +1 -1
- package/dist/ui/assets/{index-B5qGYOJo.js → index-5fXOHoVh.js} +2 -2
- package/dist/ui/assets/index-5fXOHoVh.js.map +1 -0
- package/dist/ui/index.html +1 -1
- package/docs/contracts/settings-classes.md +307 -145
- package/docs/guidelines/agent-infra/layered-settings.md +1 -2
- package/docs/guidelines/agent-infra/symptom-driven-harvest-loop.md +104 -0
- package/package.json +1 -1
- package/src/agent-src/templates/scripts/work_engine/_lib/agent_settings.ts +0 -1
- package/src/config/agent-settings.template.yml +12 -30
- package/src/config/gate-violation-baselines.json +9 -3
- package/src/config/profiles/balanced.ini +0 -2
- package/src/config/profiles/full.ini +1 -3
- package/src/config/profiles/minimal.ini +0 -2
- package/src/scripts/_cli/cmd_session_recycle.ts +66 -9
- package/src/scripts/_cli/handoff_sessions.ts +3 -2
- package/src/scripts/_lib/agent_settings.ts +25 -2
- package/src/scripts/_lib/bench_telegraph.ts +1 -1
- package/src/scripts/_lib/cc_transcript.ts +25 -0
- package/src/scripts/_lib/compile_time_toggles.ts +4 -3
- package/src/scripts/ai_council/clients.ts +228 -33
- package/src/scripts/cache_realization_report.ts +3 -2
- package/src/scripts/check_rule_projection_integrity.ts +3 -1
- package/src/scripts/conformance_scan.ts +282 -9
- package/src/scripts/cost/track.mjs +10 -4
- package/src/scripts/council_cli.ts +33 -3
- package/src/scripts/external_sources_denylist.json +3 -1
- package/src/scripts/hook_manifest.yaml +17 -6
- package/src/scripts/hooks/block_unauthorized_git.ts +54 -8
- package/src/scripts/hooks/delegation_nudge_hook.ts +33 -8
- package/src/scripts/hooks/evidence_independence.ts +61 -9
- package/src/scripts/hooks/session_eol_hook.ts +123 -3
- package/src/scripts/hooks/turn_end_gate_hook.ts +31 -0
- package/src/scripts/lint_settings_classes.ts +273 -11
- package/src/scripts/lint_symptom_intake.ts +241 -0
- package/src/scripts/report_consultation_rate.ts +2 -1
- package/src/scripts/report_skill_activation.ts +6 -4
- package/src/scripts/report_skill_obligation_violations.ts +2 -1
- package/src/server/io/yamlIO.ts +0 -2
- package/src/server/schemas/settings.ts +3 -19
- package/src/shared/settingsClasses.ts +93 -2
- package/dist/ui/assets/index-B5qGYOJo.js.map +0 -1
|
@@ -128,10 +128,35 @@ function _isObject(v: unknown): v is JsonObject {
|
|
|
128
128
|
const FILE_TOKEN_RE =
|
|
129
129
|
/`?\b[\w][\w.\-]{0,80}\.(ts|tsx|js|jsx|mjs|cjs|py|php|rb|go|rs|java|kt|swift|md|mdx|json|ya?ml|css|scss|less|html|vue|svelte|sql|sh|bash|c|cpp|h|hpp|cs|toml|xml)\b`?/gi;
|
|
130
130
|
|
|
131
|
-
|
|
131
|
+
/**
|
|
132
|
+
* The unique file tokens themselves, lower-cased and stripped of backticks.
|
|
133
|
+
*
|
|
134
|
+
* `detectEnumeratedFiles` needs only the size, but `conformance_scan`'s
|
|
135
|
+
* `task-completeness` check needs the IDENTITIES: it asks which of the files a
|
|
136
|
+
* prompt enumerated were actually touched, which a count cannot answer. Exposing
|
|
137
|
+
* them here rather than re-matching `FILE_TOKEN_RE` in the scanner keeps one
|
|
138
|
+
* regex in the tree — a second copy is the "second artefact to keep in sync"
|
|
139
|
+
* that would let the classifier and the measurement disagree silently, which is
|
|
140
|
+
* the property `conformance_scan`'s own header requires of every classifier it
|
|
141
|
+
* imports.
|
|
142
|
+
*/
|
|
143
|
+
/**
|
|
144
|
+
* Minimum unique file tokens before the FILE shape counts as a delegable
|
|
145
|
+
* signal (F7, review). Named rather than inlined because `conformance_scan`'s
|
|
146
|
+
* `task-completeness` check gates on the same floor: a prompt whose file
|
|
147
|
+
* enumeration is below it is not treated as naming a deliverable SET by either
|
|
148
|
+
* reader, and two copies of the number would let them disagree about which
|
|
149
|
+
* prompts are in scope.
|
|
150
|
+
*/
|
|
151
|
+
export const FILE_SIGNAL_FLOOR = 3;
|
|
152
|
+
|
|
153
|
+
export function enumeratedFileTokens(text: string): string[] {
|
|
132
154
|
const matches = text.match(FILE_TOKEN_RE) ?? [];
|
|
133
|
-
|
|
134
|
-
|
|
155
|
+
return [...new Set(matches.map((m) => m.replace(/`/g, "").toLowerCase()))];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function detectEnumeratedFiles(text: string): number {
|
|
159
|
+
return enumeratedFileTokens(text).length;
|
|
135
160
|
}
|
|
136
161
|
|
|
137
162
|
/**
|
|
@@ -246,12 +271,12 @@ export function extractTaskSignals(text: string): ExtractedSignals {
|
|
|
246
271
|
const conjunction = detectMultiDeliverableConjunction(text);
|
|
247
272
|
const forEach = detectForEachShape(text);
|
|
248
273
|
|
|
249
|
-
// F7 (review): a bare file-token count below
|
|
250
|
-
// evidence of independent, parallelizable work — "rename a.ts to
|
|
251
|
-
// names 2 files but is one mechanical edit, not two delegable slices.
|
|
252
|
-
// explicit "N files/Dateien" phrase (`detectExplicitSliceCount`) is a
|
|
274
|
+
// F7 (review): a bare file-token count below FILE_SIGNAL_FLOOR is common and
|
|
275
|
+
// NOT itself evidence of independent, parallelizable work — "rename a.ts to
|
|
276
|
+
// b.ts" names 2 files but is one mechanical edit, not two delegable slices.
|
|
277
|
+
// The explicit "N files/Dateien" phrase (`detectExplicitSliceCount`) is a
|
|
253
278
|
// stronger, author-stated signal and keeps its existing floor of N>=2.
|
|
254
|
-
const fileSignal = fileCount >=
|
|
279
|
+
const fileSignal = fileCount >= FILE_SIGNAL_FLOOR ? fileCount : 0;
|
|
255
280
|
|
|
256
281
|
let slices = Math.max(fileSignal, explicit, conjunction);
|
|
257
282
|
if (slices === 0 && forEach) {
|
|
@@ -135,9 +135,27 @@ export function isEvaluationPrompt(prompt: string): boolean {
|
|
|
135
135
|
* ON, so the second-dispatch block requires a self-reference. The pre-loaded-
|
|
136
136
|
* verdict block does NOT: steering an evaluator is wrong whatever it is
|
|
137
137
|
* pointed at.
|
|
138
|
+
*
|
|
139
|
+
* That fix was still too wide, and the cross-project session audit (2026-08-12)
|
|
140
|
+
* measured the cost: `road-to-release-truth/fc1ff181` turn 3 fanned out 16
|
|
141
|
+
* IMPLEMENTATION workers, and 15 were classified self-scoped on one phrase —
|
|
142
|
+
* `this branch`. In context it read "two conversions already landed on this
|
|
143
|
+
* branch in exactly the style you should match", i.e. it named WHERE the work
|
|
144
|
+
* happens. A worktree dispatch prompt carries that phrase by construction
|
|
145
|
+
* ("Work in <worktree> (branch …)"), so on a hook-bound host the guard ate 15
|
|
146
|
+
* of 16 workers — the exact behaviour `evaluator-independence` § "When it does
|
|
147
|
+
* NOT fire" promises is out of scope ("…or IMPLEMENT is not evaluation and is
|
|
148
|
+
* not gated").
|
|
149
|
+
*
|
|
150
|
+
* So the self-reference must name a SUBJECT a verdict can be shopped for — a
|
|
151
|
+
* diff, a change, a patch — never the LOCATION the work happens in. `this
|
|
152
|
+
* branch` is dropped for that reason; `my branch` stays, because the possessive
|
|
153
|
+
* makes it a claim about the agent's own work rather than an address. `this pr`
|
|
154
|
+
* stays for the same reason `this diff` does: a PR is a thing reviewers render
|
|
155
|
+
* verdicts on, whereas a branch is where you stand while working.
|
|
138
156
|
*/
|
|
139
157
|
const SELF_SCOPE_RE =
|
|
140
|
-
/\b(my (work|change|diff|implementation|code|fix|patch|branch|pr)|this (diff|change|
|
|
158
|
+
/\b(my (work|change|diff|implementation|code|fix|patch|branch|pr)|this (diff|change|pr|delta|implementation|patch)|the delta|the change i|what i (wrote|built|changed|implemented)|i just (wrote|built|changed|implemented))\b/i;
|
|
141
159
|
|
|
142
160
|
export function isSelfScoped(prompt: string): boolean {
|
|
143
161
|
return SELF_SCOPE_RE.test(prompt);
|
|
@@ -251,16 +269,50 @@ export function decide(
|
|
|
251
269
|
return { exit: EXIT_ALLOW, stdout: "", stderr: "", evaluations: priorEvaluations };
|
|
252
270
|
}
|
|
253
271
|
|
|
272
|
+
// WARN, not block — a deliberate severity decision, not an oversight.
|
|
273
|
+
//
|
|
274
|
+
// This branch decides from PROSE ALONE: `isEvaluationPrompt` and `isSelfScoped`
|
|
275
|
+
// both infer intent from a natural-language prompt, and no structured fact
|
|
276
|
+
// corroborates them. Under the tier rule in `docs/contracts/hook-architecture-v1.md`
|
|
277
|
+
// (§ What a concern may block on) that is Tier 3, and Tier 3 may only warn.
|
|
278
|
+
//
|
|
279
|
+
// It is written from measurement, not taste. A 16-way fan-out of IMPLEMENTATION
|
|
280
|
+
// subagents lost 15 workers to this branch, because `isEvaluationPrompt` fired
|
|
281
|
+
// on the unavoidable words review/audit/check and `isSelfScoped` on the phrase
|
|
282
|
+
// `this branch` — an address every worktree dispatch prompt carries. Narrowing
|
|
283
|
+
// the phrase list fixed that instance; the council convened on the design
|
|
284
|
+
// (anthropic + openai, 2026-08-12, quorum 2/2) held that no finite pattern can
|
|
285
|
+
// bound the false-positive set, and that a prompt — unlike a shell command —
|
|
286
|
+
// has no grammar to anchor an "invoked vs named" discriminator to.
|
|
287
|
+
//
|
|
288
|
+
// What is NOT downgraded: the pre-loaded-verdict block above. That one matches
|
|
289
|
+
// a literal steering formulation ("NO-FINDINGS is expected and welcome"), which
|
|
290
|
+
// IS the violation rather than evidence of one, so it keeps blocking.
|
|
291
|
+
//
|
|
292
|
+
// The route back to blocking is structural, not another regex: a `role` /
|
|
293
|
+
// `evidence_scope` field the dispatcher sets at the call site, where the caller
|
|
294
|
+
// knows by construction whether it is commissioning an evaluation. Then this
|
|
295
|
+
// branch reads a field instead of guessing, and becomes Tier 1.
|
|
296
|
+
// exit 0 + `decision: "warn"` on stdout is how this dispatcher carries an
|
|
297
|
+
// advisory — NOT exit 2. The internal ladder's `2 = warn` is read as BLOCK by
|
|
298
|
+
// Claude Code's native PreToolUse contract, which is the defect that made an
|
|
299
|
+
// advisory guard a hard deny once already; the same shape here would have kept
|
|
300
|
+
// the fan-out blocked while claiming to warn.
|
|
254
301
|
if (priorEvaluations >= 1) {
|
|
255
302
|
return {
|
|
256
|
-
exit:
|
|
257
|
-
stdout:
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
303
|
+
exit: EXIT_ALLOW,
|
|
304
|
+
stdout: `${JSON.stringify({
|
|
305
|
+
decision: "warn",
|
|
306
|
+
reason:
|
|
307
|
+
`Second evaluation dispatch of your own work in this turn. If both passes ` +
|
|
308
|
+
`judge the SAME subject, that is verdict shopping — commissioning another ` +
|
|
309
|
+
`with a different prompt or scope selects the answer instead of measuring ` +
|
|
310
|
+
`it. Report what the first pass returned, then re-plan. If this is an ` +
|
|
311
|
+
`implementation fan-out rather than a second review, carry on: this check ` +
|
|
312
|
+
`reads prose and cannot tell the two apart with certainty.`,
|
|
313
|
+
})}\n`,
|
|
314
|
+
stderr: "",
|
|
315
|
+
evaluations: priorEvaluations + 1,
|
|
264
316
|
};
|
|
265
317
|
}
|
|
266
318
|
|
|
@@ -17,6 +17,16 @@
|
|
|
17
17
|
* SILENCE, never a block (fail-open); hooks cannot inject `/clear`, so the
|
|
18
18
|
* recycle action itself stays advisory-carried by design (roadmap 5.1).
|
|
19
19
|
*
|
|
20
|
+
* Counter-check (second warn path, also once per session): when the advisory
|
|
21
|
+
* fired on an earlier Stop and no envelope written since then exists, inject
|
|
22
|
+
* one further line saying so — the advisory recommends an action whose next
|
|
23
|
+
* step destroys the session, so recommending it without ever checking the
|
|
24
|
+
* result is how a silent write failure becomes total context loss. Stamped by
|
|
25
|
+
* `missing_envelope_warned_at`, gated on the same threshold, so the emergency
|
|
26
|
+
* off-switch silences BOTH paths. Two independent emitters, at most one line
|
|
27
|
+
* each per session — the budget the Stop slot carries from this hook is two,
|
|
28
|
+
* not one.
|
|
29
|
+
*
|
|
20
30
|
* Phase 4.2 (read surface): every Stop also overwrites
|
|
21
31
|
* `agents/runtime/state/context-fill.json` with the machine-readable fill
|
|
22
32
|
* level + threshold state — display substrate for an external statusline;
|
|
@@ -35,6 +45,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
35
45
|
|
|
36
46
|
import recycleThresholdConfig from '../../config/recycle-threshold-budget.json';
|
|
37
47
|
|
|
48
|
+
import { RECYCLE_ENVELOPE_REL } from '../_lib/recycle_envelope_paths.js';
|
|
38
49
|
import {
|
|
39
50
|
emptyCounters,
|
|
40
51
|
eolSessionKey,
|
|
@@ -67,6 +78,13 @@ export interface SessionEolState {
|
|
|
67
78
|
counters: EolCounters;
|
|
68
79
|
/** ISO stamp when the recycle advisory fired for this session (F2), or null. */
|
|
69
80
|
advisory_fired_at: string | null;
|
|
81
|
+
/**
|
|
82
|
+
* ISO stamp when the follow-up "advised, but no envelope exists" line
|
|
83
|
+
* fired, or null. Separate from `advisory_fired_at` so the counter-check
|
|
84
|
+
* is once-per-session in its own right: one reminder is a safety net, one
|
|
85
|
+
* per Stop for the rest of the session is a nag the reader learns to skip.
|
|
86
|
+
*/
|
|
87
|
+
missing_envelope_warned_at?: string | null;
|
|
70
88
|
updated_at: string;
|
|
71
89
|
}
|
|
72
90
|
|
|
@@ -105,6 +123,10 @@ export function readState(file: string): SessionEolState {
|
|
|
105
123
|
counters: parsed.counters,
|
|
106
124
|
advisory_fired_at:
|
|
107
125
|
typeof parsed.advisory_fired_at === 'string' ? parsed.advisory_fired_at : null,
|
|
126
|
+
missing_envelope_warned_at:
|
|
127
|
+
typeof parsed.missing_envelope_warned_at === 'string'
|
|
128
|
+
? parsed.missing_envelope_warned_at
|
|
129
|
+
: null,
|
|
108
130
|
updated_at: typeof parsed.updated_at === 'string' ? parsed.updated_at : '',
|
|
109
131
|
};
|
|
110
132
|
}
|
|
@@ -115,10 +137,70 @@ export function readState(file: string): SessionEolState {
|
|
|
115
137
|
schema_version: 1,
|
|
116
138
|
counters: emptyCounters(),
|
|
117
139
|
advisory_fired_at: null,
|
|
140
|
+
missing_envelope_warned_at: null,
|
|
118
141
|
updated_at: '',
|
|
119
142
|
};
|
|
120
143
|
}
|
|
121
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Is there a recycle envelope under this workspace, written since `since`?
|
|
147
|
+
*
|
|
148
|
+
* Two properties, both load-bearing:
|
|
149
|
+
*
|
|
150
|
+
* **Only ENOENT counts as missing.** `fs.existsSync` swallows every error into
|
|
151
|
+
* `false`, so an unreadable directory (EACCES on a mounted or root-owned tree)
|
|
152
|
+
* would read as "no envelope" and produce the manufactured "your envelope is
|
|
153
|
+
* gone" warning this check exists to avoid. `statSync` + an errno test is the
|
|
154
|
+
* only shape that can tell the two apart.
|
|
155
|
+
*
|
|
156
|
+
* **Freshness, not mere existence.** The consumer moves the envelope aside at
|
|
157
|
+
* session_start, so a file still sitting here belongs to a session that never
|
|
158
|
+
* cleared. Counting it would silence the warning in the case it exists to
|
|
159
|
+
* catch, and worse: `/clear` would then resume the successor from another
|
|
160
|
+
* session's state — a wrong resume instead of an empty one. An envelope whose
|
|
161
|
+
* `written_at` predates the advisory is not this session's.
|
|
162
|
+
*/
|
|
163
|
+
export function envelopeExists(workspaceRoot: string, since: string | null = null): boolean {
|
|
164
|
+
const target = path.join(workspaceRoot, RECYCLE_ENVELOPE_REL);
|
|
165
|
+
let raw: string;
|
|
166
|
+
try {
|
|
167
|
+
raw = fs.readFileSync(target, 'utf-8');
|
|
168
|
+
} catch (exc) {
|
|
169
|
+
// ENOENT is the real "no envelope"; anything else is a failed CHECK,
|
|
170
|
+
// and a failed check must never assert absence.
|
|
171
|
+
return (exc as NodeJS.ErrnoException)?.code !== 'ENOENT';
|
|
172
|
+
}
|
|
173
|
+
if (since === null) return true;
|
|
174
|
+
try {
|
|
175
|
+
const written = (JSON.parse(raw) as { written_at?: unknown }).written_at;
|
|
176
|
+
// Unparseable or undated: present, and not provably stale. Treat it as
|
|
177
|
+
// this session's — the alternative warns about a file that is there.
|
|
178
|
+
if (typeof written !== 'string') return true;
|
|
179
|
+
return Date.parse(written) >= Date.parse(since);
|
|
180
|
+
} catch {
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* The follow-up line: the advisory already fired, and no envelope arrived.
|
|
187
|
+
*
|
|
188
|
+
* This is the half of the reported minimum fix that lives outside the command
|
|
189
|
+
* — the hook that recommends the call making the same counter-check before the
|
|
190
|
+
* operator acts on it. Whatever stopped the write (a wrong root, a refusal
|
|
191
|
+
* scrolled past, a forgotten step), the state visible here is identical, and
|
|
192
|
+
* the next action the advisory recommended is the one that destroys the
|
|
193
|
+
* session.
|
|
194
|
+
*/
|
|
195
|
+
export function buildMissingEnvelopeLine(workspaceRoot: string): string {
|
|
196
|
+
return (
|
|
197
|
+
`recycle advised earlier, but no envelope exists at ` +
|
|
198
|
+
`${path.join(workspaceRoot, RECYCLE_ENVELOPE_REL)} — /clear now starts the successor ` +
|
|
199
|
+
`from nothing. Re-run \`agent-config session:recycle\` and check that it prints the ` +
|
|
200
|
+
`absolute path it wrote before clearing.`
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
122
204
|
/**
|
|
123
205
|
* Resolve the recycle threshold (Phase 3.1): the per-process override wins
|
|
124
206
|
* (tests; `0` = advisory lane off), else the committed constant from
|
|
@@ -136,13 +218,22 @@ export function readThresholdTokens(): number | null {
|
|
|
136
218
|
return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : null;
|
|
137
219
|
}
|
|
138
220
|
|
|
139
|
-
/**
|
|
221
|
+
/**
|
|
222
|
+
* The exactly-one advisory line injected past threshold (Phase 3.2).
|
|
223
|
+
*
|
|
224
|
+
* The `/clear` half is conditional on purpose. An earlier wording read
|
|
225
|
+
* `run X … then /clear`, which is an instruction to destroy the session with
|
|
226
|
+
* no check in between — and a reader who followed it while the command was
|
|
227
|
+
* silently writing nothing lost everything. The proof to wait for is the
|
|
228
|
+
* absolute path the command prints; naming it costs one clause.
|
|
229
|
+
*/
|
|
140
230
|
export function buildAdvisoryLine(tokens: number, threshold: number): string {
|
|
141
231
|
return (
|
|
142
232
|
`context past recycle threshold (${tokens.toLocaleString('en-US')} of ` +
|
|
143
233
|
`${threshold.toLocaleString('en-US')} tokens): run \`agent-config session:recycle\` to ` +
|
|
144
|
-
`write the recycle envelope
|
|
145
|
-
`envelope at
|
|
234
|
+
`write the recycle envelope. It prints the absolute path it wrote — /clear only after ` +
|
|
235
|
+
`you have seen that line. The successor session then resumes from the envelope at ` +
|
|
236
|
+
`session_start (road-to-token-economy-recycling)`
|
|
146
237
|
);
|
|
147
238
|
}
|
|
148
239
|
|
|
@@ -213,10 +304,29 @@ export function main(): number {
|
|
|
213
304
|
tokens >= threshold &&
|
|
214
305
|
state.advisory_fired_at === null;
|
|
215
306
|
|
|
307
|
+
// The counter-check: the advisory fired on an EARLIER Stop (so `state`,
|
|
308
|
+
// the pre-update snapshot, already carries the stamp) and no envelope has
|
|
309
|
+
// appeared since. It can never collide with `shouldAdvise` — that branch
|
|
310
|
+
// requires `advisory_fired_at === null` — so at most one line is emitted
|
|
311
|
+
// per Stop, and this one at most once per session.
|
|
312
|
+
//
|
|
313
|
+
// `threshold !== null` gates it for the same reason the advisory is gated:
|
|
314
|
+
// `AGENT_RECYCLE_THRESHOLD_TOKENS=0` is documented as the emergency switch
|
|
315
|
+
// for the whole advisory lane, and a lane that keeps one more warn after
|
|
316
|
+
// being switched off is not off.
|
|
317
|
+
const shouldWarnMissing =
|
|
318
|
+
threshold !== null &&
|
|
319
|
+
state.advisory_fired_at !== null &&
|
|
320
|
+
(state.missing_envelope_warned_at ?? null) === null &&
|
|
321
|
+
!envelopeExists(workspaceRoot, state.advisory_fired_at);
|
|
322
|
+
|
|
216
323
|
const nextState: SessionEolState = {
|
|
217
324
|
schema_version: 1,
|
|
218
325
|
counters,
|
|
219
326
|
advisory_fired_at: shouldAdvise ? now : state.advisory_fired_at,
|
|
327
|
+
missing_envelope_warned_at: shouldWarnMissing
|
|
328
|
+
? now
|
|
329
|
+
: (state.missing_envelope_warned_at ?? null),
|
|
220
330
|
updated_at: now,
|
|
221
331
|
};
|
|
222
332
|
try {
|
|
@@ -237,6 +347,16 @@ export function main(): number {
|
|
|
237
347
|
);
|
|
238
348
|
return EXIT_WARN;
|
|
239
349
|
}
|
|
350
|
+
if (shouldWarnMissing) {
|
|
351
|
+
process.stdout.write(
|
|
352
|
+
`${JSON.stringify({
|
|
353
|
+
decision: 'warn',
|
|
354
|
+
reason: 'session-eol: recycle advised, no envelope written',
|
|
355
|
+
additional_context: buildMissingEnvelopeLine(workspaceRoot),
|
|
356
|
+
})}\n`,
|
|
357
|
+
);
|
|
358
|
+
return EXIT_WARN;
|
|
359
|
+
}
|
|
240
360
|
return 0;
|
|
241
361
|
}
|
|
242
362
|
|
|
@@ -400,6 +400,34 @@ export function readCiSettled(workspaceRoot: string): { seen: boolean; settled:
|
|
|
400
400
|
const _COMPLETION_RE =
|
|
401
401
|
/(^|\n)\s*(?:\*\*)?(?:fertig\b|damit ist alles erledigt|aufgabe erledigt|alles erledigt\b|komplett(?:er)? (?:auftrag|abgearbeitet)|der komplette auftrag ist durch|done[.!]|all done\b|task complete)/i;
|
|
402
402
|
|
|
403
|
+
/**
|
|
404
|
+
* The line the claim was found on says the opposite of a claim.
|
|
405
|
+
*
|
|
406
|
+
* A line-anchored keyword cannot tell "Fertig." from "Fertig ist der Fix noch
|
|
407
|
+
* nicht." — both open with the same token, and the second is exactly the honest
|
|
408
|
+
* status report this gate exists to ENCOURAGE. Measured 2026-08-12: three of ten
|
|
409
|
+
* realistic closings were refused, and all three were "not done yet" lines.
|
|
410
|
+
*
|
|
411
|
+
* This is a negation check on the SAME line, not another attempt to enumerate
|
|
412
|
+
* completion phrasings. The council that reviewed this guard (anthropic +
|
|
413
|
+
* openai, 2026-08-12) stated the bound plainly: a finite pattern cannot cover an
|
|
414
|
+
* infinite false-positive set, so extending the keyword list again would repeat
|
|
415
|
+
* the move that has already failed three times on the sibling git guard. What
|
|
416
|
+
* makes a block defensible here is not the pattern — it is that the pattern only
|
|
417
|
+
* fires as a TRIGGER to consult structured CI state (`ci.settled`), which is the
|
|
418
|
+
* council's Tier-2 shape. This check removes the cases where the prose itself
|
|
419
|
+
* already contradicts the trigger.
|
|
420
|
+
*/
|
|
421
|
+
const _NEGATED_CLAIM_RE =
|
|
422
|
+
/\b(noch nicht|nicht fertig|nicht durch|wäre verfrüht|ist verfrüht|not yet|not done|isn'?t done|nein)\b/i;
|
|
423
|
+
|
|
424
|
+
/** The line `at` falls on — the scope a negation has to appear in to count. */
|
|
425
|
+
function _lineAround(prose: string, at: number): string {
|
|
426
|
+
const start = prose.lastIndexOf('\n', at) + 1;
|
|
427
|
+
const end = prose.indexOf('\n', at);
|
|
428
|
+
return prose.slice(start, end === -1 ? prose.length : end);
|
|
429
|
+
}
|
|
430
|
+
|
|
403
431
|
export function detectCompletionClaim(
|
|
404
432
|
reply: string,
|
|
405
433
|
ci: { seen: boolean; settled: boolean },
|
|
@@ -410,6 +438,9 @@ export function detectCompletionClaim(
|
|
|
410
438
|
const m = _COMPLETION_RE.exec(prose);
|
|
411
439
|
if (!m) return null;
|
|
412
440
|
const at = m.index;
|
|
441
|
+
// The trigger fired, but the line it fired on negates it — a status report,
|
|
442
|
+
// not a claim. Refusing these punishes exactly the honesty the gate wants.
|
|
443
|
+
if (_NEGATED_CLAIM_RE.test(_lineAround(prose, at))) return null;
|
|
413
444
|
return {
|
|
414
445
|
detector: 'completion',
|
|
415
446
|
evidence: prose.slice(at, at + 120).trim(),
|