@event4u/agent-config 9.33.0 → 9.35.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 +154 -145
- package/CONTRIBUTING.md +23 -14
- package/dist/agent-src/commands/agent-handoff.md +15 -3
- package/dist/agent-src/commands/roadmap/next.md +37 -5
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +35 -1
- package/dist/agent-src/scripts/roadmap_gates.ts +119 -4
- package/dist/agent-src/skills/corpus-grounding/SKILL.md +21 -12
- package/dist/agent-src/skills/design-tokens/SKILL.md +15 -0
- package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +24 -0
- package/dist/agent-src/skills/rule-writing/SKILL.md +23 -0
- package/dist/agent-src/skills/skill-writing/SKILL.md +80 -2
- package/dist/agent-src/skills/systematic-debugging/SKILL.md +5 -3
- package/dist/agent-src/skills/tailwind-engineer/SKILL.md +24 -0
- package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +25 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/scripts/memory_lookup.ts +1 -2
- package/dist/agent-src/templates/scripts/memory_signal.ts +2 -4
- package/dist/agent-src/templates/scripts/work_engine/hooks/builtin/decision_gate.ts +35 -5
- package/dist/agent-src/templates/scripts/work_engine/hooks/builtin/staged_confirmation.ts +252 -0
- package/dist/agent-src/templates/scripts/work_engine/hooks/builtin/staged_confirmation_store.ts +307 -0
- package/dist/cli/registry.js +2 -2
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-PMQ6Z3T3.js → chunk-2TGMGLBL.js} +11 -4
- package/dist/cli-delegate/{chunk-YPAWXXXR.js → chunk-55RI5UXK.js} +17 -0
- package/dist/cli-delegate/{chunk-MA4GFWDY.js → chunk-H6F2CQFI.js} +1 -1
- package/dist/cli-delegate/{chunk-2WELHO3P.js → chunk-I5EAFRII.js} +35 -0
- package/dist/cli-delegate/{chunk-3EWYC3YK.js → chunk-UEZ7D35A.js} +4 -3
- package/dist/cli-delegate/{chunk-UVKNIJH2.js → chunk-ZAXH3ZWU.js} +3 -0
- package/dist/cli-delegate/cmd_conformance.js +2 -2
- package/dist/cli-delegate/cmd_doctor.js +2 -2
- package/dist/cli-delegate/cmd_export.js +2 -2
- package/dist/cli-delegate/cmd_handoff.js +3 -3
- package/dist/cli-delegate/cmd_memory_get.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +2 -2
- package/dist/cli-delegate/cmd_route_audit.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +7 -2
- package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +2 -2
- package/dist/cli-delegate/cmd_uninstall.js +2 -2
- package/dist/cli-delegate/cmd_validate.js +2 -2
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +13 -13
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +2 -2
- package/dist/discovery/dormancy-report.md +17 -0
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +3 -3
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +3 -3
- package/dist/hooks/dispatch.js +355 -85
- package/dist/install/install.mjs +3 -0
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +192 -12
- package/dist/server/schemas/settings.js +1 -0
- package/dist/server/schemas/settings.js.map +1 -1
- package/docs/contracts/settings-classes.md +3 -2
- package/docs/guidelines/agent-infra/failure-signatures.md +47 -12
- package/package.json +2 -2
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +35 -1
- package/src/agent-src/scripts/roadmap_gates.ts +119 -4
- package/src/agent-src/templates/scripts/memory_lookup.ts +1 -2
- package/src/agent-src/templates/scripts/memory_signal.ts +2 -4
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/decision_gate.ts +35 -5
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/staged_confirmation.ts +252 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/staged_confirmation_store.ts +307 -0
- package/src/config/agent-settings.template.yml +11 -2
- package/src/config/quorum-attendance-budget.json +11 -3
- package/src/scripts/_cli/cmd_session_recycle.ts +6 -1
- package/src/scripts/_dispatch.bash +6 -2
- package/src/scripts/_lib/env_kill_switch.ts +37 -0
- package/src/scripts/_lib/security_lint.ts +18 -4
- package/src/scripts/_lib/self_repair.ts +60 -0
- package/src/scripts/_lib/self_repair_store.ts +65 -5
- package/src/scripts/_lib/session_register.ts +67 -1
- package/src/scripts/_lib/subagent_capsule.ts +78 -1
- package/src/scripts/ai_council/config.ts +60 -0
- package/src/scripts/ai_council/events_log.ts +105 -6
- package/src/scripts/ai_council/orchestrator.ts +38 -0
- package/src/scripts/ai_council/prompts.ts +165 -3
- package/src/scripts/ai_council/quorum.ts +87 -0
- package/src/scripts/ai_team/review_gate.ts +2 -2
- package/src/scripts/build_discovery_manifest.ts +165 -0
- package/src/scripts/check_claims.ts +51 -2
- package/src/scripts/ci_time_ratio.ts +3 -1
- package/src/scripts/council_cli.ts +36 -1
- package/src/scripts/discovery_graph.ts +201 -27
- package/src/scripts/dispatch_r2_reviewer.ts +102 -1
- package/src/scripts/hooks/turn_end_gate_hook.ts +172 -11
- package/src/scripts/lint_framework_leakage.ts +195 -28
- package/src/scripts/lint_framework_leakage_allowlist.json +94 -86
- package/src/scripts/lint_handoffs.ts +92 -2
- package/src/scripts/lint_hedge_words.ts +317 -0
- package/src/scripts/lint_mcp_config_security.ts +1 -1
- package/src/scripts/lint_skill_frontmatter_safety.ts +1 -1
- package/src/scripts/memory_lookup.ts +1 -2
- package/src/scripts/schemas/command.schema.json +4 -0
- package/src/scripts/schemas/skill.schema.json +4 -0
- package/src/scripts/self_repair_cli.ts +26 -1
- package/src/scripts/session_register_hook.ts +238 -18
- package/src/scripts/sessions_cli.ts +377 -12
- package/src/scripts/skill_linter.ts +58 -2
- package/src/server/schemas/settings.ts +3 -0
|
@@ -128,12 +128,42 @@ function _branch_touched_paths(root: string, base: string): Set<string> | null {
|
|
|
128
128
|
return out;
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Paths where a repo-relative path is DATA, not a live reference — so
|
|
133
|
+
* rewriting it falsifies a record instead of keeping a link resolvable.
|
|
134
|
+
*
|
|
135
|
+
* Two classes, and the second is why this predicate exists at all:
|
|
136
|
+
*
|
|
137
|
+
* - `agents/evidence/**` — a committed record of what was true, or of what a
|
|
138
|
+
* reviewer was shown, at one point in time. Retroactively editing it makes it
|
|
139
|
+
* claim something that did not happen, which is the failure
|
|
140
|
+
* `evaluator-independence` exists to prevent ("the prompt ships with the
|
|
141
|
+
* verdict" is worthless if a later sweep can edit the prompt).
|
|
142
|
+
* - `*.patch` anywhere — a patch's `diff --git a/… b/…` and `+++ b/…` lines are
|
|
143
|
+
* the payload. Rewriting them detaches the patch from the commit it records
|
|
144
|
+
* AND makes `git apply` target a path the diff never created.
|
|
145
|
+
*
|
|
146
|
+
* Measured 2026-08-11: archiving one roadmap rewrote four frozen artefacts
|
|
147
|
+
* under `agents/evidence/reviews/`, including the `diff.patch` headers of a
|
|
148
|
+
* recorded review input. Nothing caught it — the sweep reported it as
|
|
149
|
+
* "4 ref(s) migrated", which reads like success.
|
|
150
|
+
*/
|
|
151
|
+
const _FROZEN_RECORD_PREFIXES: readonly string[] = ['agents/evidence/'];
|
|
152
|
+
|
|
153
|
+
/** Is `rel` a frozen record whose path strings must not be rewritten? */
|
|
154
|
+
export function _is_frozen_record(rel: string): boolean {
|
|
155
|
+
const norm = rel.split(path.sep).join('/');
|
|
156
|
+
return _FROZEN_RECORD_PREFIXES.some((p) => norm.startsWith(p)) || norm.endsWith('.patch');
|
|
157
|
+
}
|
|
158
|
+
|
|
131
159
|
/**
|
|
132
160
|
* Rewrite full-path references `old_rel` → `new_rel` in tracked files.
|
|
133
161
|
*
|
|
134
162
|
* Only the exact repo-relative path is rewritten (bare-filename mentions are
|
|
135
163
|
* left alone). The archived file's own path never matches because the search
|
|
136
|
-
* string is the un-archived path.
|
|
164
|
+
* string is the un-archived path. Frozen records are skipped — see
|
|
165
|
+
* `_is_frozen_record`; a link left dangling in a historical record is correct,
|
|
166
|
+
* because the record describes a tree that no longer exists.
|
|
137
167
|
*/
|
|
138
168
|
function _inbound_ref_rewrite(
|
|
139
169
|
root: string,
|
|
@@ -165,6 +195,10 @@ function _inbound_ref_rewrite(
|
|
|
165
195
|
// skip the roadmap file itself
|
|
166
196
|
continue;
|
|
167
197
|
}
|
|
198
|
+
if (_is_frozen_record(rel)) {
|
|
199
|
+
// A record of a past state — rewriting it falsifies the record.
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
168
202
|
const fp = path.join(root, rel);
|
|
169
203
|
let text: string;
|
|
170
204
|
try {
|
|
@@ -46,6 +46,13 @@
|
|
|
46
46
|
* ./agent-config gates --all # every open blocker, grouped by owner
|
|
47
47
|
* ./agent-config gates --json # machine-readable
|
|
48
48
|
* ./agent-config gates --reply # reply-close form; empty when none
|
|
49
|
+
* ./agent-config gates --pending # staged actions awaiting confirmation
|
|
50
|
+
*
|
|
51
|
+
* `--pending` reads a different source — the staged-confirmation store, not the
|
|
52
|
+
* roadmap tree — and stays out of `--reply` on purpose. ADR-222 fixes the
|
|
53
|
+
* reply-close form at exactly ONE decision rendered in full; folding a second
|
|
54
|
+
* source into it would silently change which decision "the one" is, and that
|
|
55
|
+
* contract is not this step's to move.
|
|
49
56
|
*/
|
|
50
57
|
|
|
51
58
|
import * as path from 'node:path';
|
|
@@ -58,6 +65,8 @@ import {
|
|
|
58
65
|
blocker_needs_user as needsUser,
|
|
59
66
|
type Blocker,
|
|
60
67
|
} from './update_roadmap_progress.js';
|
|
68
|
+
import { listPending } from '../templates/scripts/work_engine/hooks/builtin/staged_confirmation_store.js';
|
|
69
|
+
import type { StagedAction } from '../templates/scripts/work_engine/hooks/builtin/staged_confirmation.js';
|
|
61
70
|
|
|
62
71
|
const _HERE = fileURLToPath(import.meta.url);
|
|
63
72
|
|
|
@@ -327,6 +336,89 @@ function renderJson(entries: readonly Entry[], all: boolean): string {
|
|
|
327
336
|
);
|
|
328
337
|
}
|
|
329
338
|
|
|
339
|
+
/**
|
|
340
|
+
* `--pending`: staged actions awaiting a human confirmation.
|
|
341
|
+
*
|
|
342
|
+
* A second source under the same verb, and the reason it belongs here rather
|
|
343
|
+
* than in a new command is the verb's own definition — "open decisions that
|
|
344
|
+
* need you, as actions". A staged irreversible action IS one, and the most
|
|
345
|
+
* literal kind: the roadmap blockers above are decisions a human owes a plan,
|
|
346
|
+
* this is a decision a human owes an action that is already loaded and waiting.
|
|
347
|
+
*
|
|
348
|
+
* Expired stages are counted, never listed as actionable. An expired stage
|
|
349
|
+
* cannot execute (`stageStatus` derives that from the clock, not from a sweep
|
|
350
|
+
* having run), so rendering it beside a live one would put two rows in front of
|
|
351
|
+
* the reader where only one is a decision.
|
|
352
|
+
*/
|
|
353
|
+
function renderPending(root: string, now: number): string {
|
|
354
|
+
const rows = listPending(root, now);
|
|
355
|
+
const live = rows.filter((r) => r.status === 'pending');
|
|
356
|
+
const expired = rows.filter((r) => r.status === 'expired');
|
|
357
|
+
const lines: string[] = [];
|
|
358
|
+
|
|
359
|
+
if (live.length === 0) {
|
|
360
|
+
lines.push(
|
|
361
|
+
expired.length === 0
|
|
362
|
+
? 'No staged actions awaiting confirmation.'
|
|
363
|
+
: `Nothing awaits your confirmation. ${expired.length} stage` +
|
|
364
|
+
`${expired.length !== 1 ? 's' : ''} expired unconfirmed — the action never fired.`,
|
|
365
|
+
);
|
|
366
|
+
return lines.join('\n') + '\n';
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
lines.push(
|
|
370
|
+
`${live.length} staged action${live.length !== 1 ? 's' : ''} await` +
|
|
371
|
+
`${live.length === 1 ? 's' : ''} your confirmation` +
|
|
372
|
+
(expired.length > 0 ? ` · ${expired.length} expired unconfirmed` : ''),
|
|
373
|
+
);
|
|
374
|
+
let n = 0;
|
|
375
|
+
for (const { stage } of live) {
|
|
376
|
+
n += 1;
|
|
377
|
+
lines.push(...renderStage(stage, n));
|
|
378
|
+
}
|
|
379
|
+
lines.push('');
|
|
380
|
+
return lines.join('\n') + '\n';
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function renderStage(stage: StagedAction, index: number): string[] {
|
|
384
|
+
const head = `${index} · ${stage.action}`;
|
|
385
|
+
const tail = `token: ${stage.token}`;
|
|
386
|
+
const dashes = Math.max(3, WIDTH - head.length - tail.length - 6);
|
|
387
|
+
return [
|
|
388
|
+
'',
|
|
389
|
+
`── ${head} ${'─'.repeat(dashes)} ${tail}`.trimEnd(),
|
|
390
|
+
// The object is the first field on purpose: an approval that does not
|
|
391
|
+
// name the exact object is the thing non-destructive-by-default forbids,
|
|
392
|
+
// so it is what the reader must see before anything else.
|
|
393
|
+
...field('Object:', stage.object),
|
|
394
|
+
...field('Staged by:', stage.source),
|
|
395
|
+
...field('Expires:', stage.expires_at),
|
|
396
|
+
];
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function renderPendingJson(root: string, now: number): string {
|
|
400
|
+
const rows = listPending(root, now);
|
|
401
|
+
return (
|
|
402
|
+
JSON.stringify(
|
|
403
|
+
{
|
|
404
|
+
awaitingYou: rows.filter((r) => r.status === 'pending').length,
|
|
405
|
+
expired: rows.filter((r) => r.status === 'expired').length,
|
|
406
|
+
staged: rows.map(({ stage, status }) => ({
|
|
407
|
+
token: stage.token,
|
|
408
|
+
action: stage.action,
|
|
409
|
+
object: stage.object,
|
|
410
|
+
source: stage.source,
|
|
411
|
+
stagedAt: stage.staged_at,
|
|
412
|
+
expiresAt: stage.expires_at,
|
|
413
|
+
status,
|
|
414
|
+
})),
|
|
415
|
+
},
|
|
416
|
+
null,
|
|
417
|
+
2,
|
|
418
|
+
) + '\n'
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
|
|
330
422
|
function _isDir(p: string): boolean {
|
|
331
423
|
try {
|
|
332
424
|
return fs.statSync(p).isDirectory();
|
|
@@ -340,8 +432,8 @@ function _isDir(p: string): boolean {
|
|
|
340
432
|
* `agents/roadmaps`, otherwise fall back to the git toplevel (monorepo
|
|
341
433
|
* sub-project support).
|
|
342
434
|
*/
|
|
343
|
-
function _resolveRepoRoot(start: string): string {
|
|
344
|
-
if (_isDir(path.join(start,
|
|
435
|
+
function _resolveRepoRoot(start: string, marker = path.join('agents', 'roadmaps')): string {
|
|
436
|
+
if (_isDir(path.join(start, marker))) {
|
|
345
437
|
return start;
|
|
346
438
|
}
|
|
347
439
|
try {
|
|
@@ -351,7 +443,7 @@ function _resolveRepoRoot(start: string): string {
|
|
|
351
443
|
timeout: 10_000,
|
|
352
444
|
});
|
|
353
445
|
const top = (r.stdout || '').trim();
|
|
354
|
-
if (r.status === 0 && top !== '' && _isDir(path.join(top,
|
|
446
|
+
if (r.status === 0 && top !== '' && _isDir(path.join(top, marker))) {
|
|
355
447
|
return top;
|
|
356
448
|
}
|
|
357
449
|
} catch {
|
|
@@ -365,6 +457,18 @@ function main(argv?: readonly string[]): number {
|
|
|
365
457
|
const all = args.includes('--all');
|
|
366
458
|
const json = args.includes('--json');
|
|
367
459
|
const reply = args.includes('--reply');
|
|
460
|
+
const pending = args.includes('--pending');
|
|
461
|
+
|
|
462
|
+
// `--pending` resolves against `agents/runtime/`, not `agents/roadmaps/`,
|
|
463
|
+
// and is answered BEFORE the roadmaps-directory exit below: a staged action
|
|
464
|
+
// is independent of whether this project plans in roadmaps at all, so
|
|
465
|
+
// "no roadmaps here" is not an answer to "what awaits my confirmation".
|
|
466
|
+
if (pending) {
|
|
467
|
+
const root = _resolveRepoRoot(process.cwd(), path.join('agents', 'runtime'));
|
|
468
|
+
const now = Date.now();
|
|
469
|
+
process.stdout.write(json ? renderPendingJson(root, now) : renderPending(root, now));
|
|
470
|
+
return 0;
|
|
471
|
+
}
|
|
368
472
|
|
|
369
473
|
const repoRoot = _resolveRepoRoot(process.cwd());
|
|
370
474
|
const roadmapRoot = path.join(repoRoot, 'agents', 'roadmaps');
|
|
@@ -412,5 +516,16 @@ if (_isCliEntry()) {
|
|
|
412
516
|
process.exitCode = main();
|
|
413
517
|
}
|
|
414
518
|
|
|
415
|
-
export {
|
|
519
|
+
export {
|
|
520
|
+
main,
|
|
521
|
+
needsUser,
|
|
522
|
+
regroupTodo,
|
|
523
|
+
wrap,
|
|
524
|
+
collectEntries,
|
|
525
|
+
render,
|
|
526
|
+
renderJson,
|
|
527
|
+
renderReply,
|
|
528
|
+
renderPending,
|
|
529
|
+
renderPendingJson,
|
|
530
|
+
};
|
|
416
531
|
export type { Entry };
|
|
@@ -184,18 +184,27 @@ Each domain ships `data/manifest.json` beside its CSVs — declaring its
|
|
|
184
184
|
- BM25 tokenizer drops tokens ≤2 chars — "UI", "a11y" style queries need
|
|
185
185
|
longer companions.
|
|
186
186
|
|
|
187
|
-
##
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
- **
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
187
|
+
## Security constraints (Step 1.6 runtime-safety record)
|
|
188
|
+
|
|
189
|
+
Four scripts ship under `scripts/`: `ground.ts` (CLI), `bm25_search.ts`,
|
|
190
|
+
`decision_engine.ts`, `schema_validator.ts`.
|
|
191
|
+
|
|
192
|
+
- **What they may touch** — corpus CSVs and the manifest under the
|
|
193
|
+
manifest's own directory. `resolve_data_path` refuses absolute paths and
|
|
194
|
+
`..` escapes, so the manifest directory is the read boundary.
|
|
195
|
+
- **What they must never do** — read outside that directory, reach the
|
|
196
|
+
network, or spawn a subprocess. Embeddings and remote retrievers are
|
|
197
|
+
intentionally not implemented (ADR-061 §2), and that is a containment
|
|
198
|
+
decision, not a missing feature.
|
|
199
|
+
- **Default invocation** — read-only. The single write surface is opt-in
|
|
200
|
+
`--persist DIR`, which writes `MASTER.md` (and `pages/<page>.md` when
|
|
201
|
+
`--page` is given) under `DIR/design-system/<project-slug>/`. Nothing
|
|
202
|
+
else writes.
|
|
203
|
+
- **Outbound** — nothing. No network access at all.
|
|
204
|
+
|
|
205
|
+
One qualified exception: `rules_module` executes a manifest-adjacent
|
|
206
|
+
TypeScript module via dynamic `import()`. That is the same trust domain as
|
|
207
|
+
the skill shipping the manifest, and its containment is `resolve_data_path`.
|
|
199
208
|
|
|
200
209
|
## See also
|
|
201
210
|
|
|
@@ -113,6 +113,21 @@ four-operation split assigns to *rules/linters*, not the corpus).
|
|
|
113
113
|
3. `validate` report — exit 0 evidence, or the violations list handed to
|
|
114
114
|
the polish round as `token_violation` findings.
|
|
115
115
|
|
|
116
|
+
## Security constraints
|
|
117
|
+
|
|
118
|
+
`scripts/tokens.ts` is the only shipped script.
|
|
119
|
+
|
|
120
|
+
- **What it may touch** — the `tokens.json` config path it is given, the
|
|
121
|
+
directory it is asked to `validate`, and the `--output` path it is asked
|
|
122
|
+
to write. Nothing outside the paths named on the command line.
|
|
123
|
+
- **What it must never do** — reach the network, spawn a subprocess, or
|
|
124
|
+
write to a path the caller did not name. It never edits the generated CSS
|
|
125
|
+
it produced earlier; regeneration replaces, it does not patch.
|
|
126
|
+
- **Default invocation** — read-only. `generate` prints to stdout unless
|
|
127
|
+
`--output PATH` is passed; `--output` is the flag that makes it mutating,
|
|
128
|
+
and it creates only that file's parent directory. `validate` never writes.
|
|
129
|
+
- **Outbound** — nothing. No network access.
|
|
130
|
+
|
|
116
131
|
## Do NOT
|
|
117
132
|
|
|
118
133
|
- Do NOT hand-edit generated CSS — `tokens.json` is the single source.
|
|
@@ -299,6 +299,30 @@ carve-out for the tool-specific part.
|
|
|
299
299
|
values the component no longer accepts is stale documentation; keep them beside
|
|
300
300
|
the component and update them in the same change.
|
|
301
301
|
|
|
302
|
+
## Security constraints
|
|
303
|
+
|
|
304
|
+
`scripts/shadcn_add.ts` is the only shipped script, and it is the single
|
|
305
|
+
subprocess-plus-network surface in this suite — treat it accordingly.
|
|
306
|
+
|
|
307
|
+
- **What it may touch** — the project rooted at `--project-root` (default
|
|
308
|
+
`cwd`): it reads `components.json` there and lets the upstream CLI write
|
|
309
|
+
the generated primitives under that root. Nothing outside it.
|
|
310
|
+
- **What it must never do** — run without the exact `npx` command and the
|
|
311
|
+
component list having been shown to the user first, per the gate above.
|
|
312
|
+
Never pass `--overwrite` unprompted; never treat a non-zero exit as
|
|
313
|
+
success; never work around a missing `npx` silently
|
|
314
|
+
([`missing-tool-handling`](../../rules/missing-tool-handling.md)).
|
|
315
|
+
- **Default invocation** — mutating. A bare `shadcn_add <component>`
|
|
316
|
+
spawns `npx shadcn@latest add <component>` and writes into the project.
|
|
317
|
+
`--dry-run` is the read-only path and prints the command it would run;
|
|
318
|
+
`--list` is read-only too. Use `--dry-run` for the proposal step.
|
|
319
|
+
- **Outbound** — yes, and this is the point of the gate: `npx` resolves
|
|
320
|
+
`shadcn@latest` from the public npm registry on every live run, so both
|
|
321
|
+
the code fetched and the components written are chosen upstream, at run
|
|
322
|
+
time, not pinned here. That is the egress leg of the lethal trifecta
|
|
323
|
+
([`lethal-trifecta-guard`](../../rules/lethal-trifecta-guard.md)); the
|
|
324
|
+
human confirmation is what keeps it off an autonomous path.
|
|
325
|
+
|
|
302
326
|
## Do NOT
|
|
303
327
|
|
|
304
328
|
- Do NOT install `shadcn-ui` from npm — primitives are scaffolded.
|
|
@@ -355,3 +355,26 @@ Good description (trigger-shaped, names domain + symptoms):
|
|
|
355
355
|
Bad description (no trigger, too vague):
|
|
356
356
|
|
|
357
357
|
> "Commit conventions"
|
|
358
|
+
|
|
359
|
+
### Contrastive-example slot (optional, for the rule body)
|
|
360
|
+
|
|
361
|
+
The pair above governs descriptions. A rule whose obligation is easy to agree
|
|
362
|
+
with and hard to apply — what counts as a cheap question, when an interrupt is
|
|
363
|
+
an interrupt, which reply mirrors the user's language — needs the same treatment
|
|
364
|
+
for the **behaviour**, not just the frontmatter.
|
|
365
|
+
|
|
366
|
+
Six live corpora already carry those pairs:
|
|
367
|
+
[`direct-answers-demos`](../../../docs/guidelines/agent-infra/direct-answers-demos.md),
|
|
368
|
+
[`asking-and-brevity-examples`](../../../docs/guidelines/agent-infra/asking-and-brevity-examples.md),
|
|
369
|
+
[`language-and-tone-examples`](../../../docs/guidelines/agent-infra/language-and-tone-examples.md),
|
|
370
|
+
and `autonomy-examples` / `interrupt-examples` / `cheap-question-mechanics` under
|
|
371
|
+
`src/agent-src/contexts/execution/`. Follow one; do not invent a format.
|
|
372
|
+
|
|
373
|
+
The shape they share: the **wrong** version in the form it actually gets
|
|
374
|
+
written, the **right** version, and one line of **why** — the why is what makes
|
|
375
|
+
it a rule rather than a memorised case.
|
|
376
|
+
|
|
377
|
+
Where the pairs go is a size decision, not a taste one. A rule body is capped at
|
|
378
|
+
200 lines hard / 120 soft, so more than two or three pairs belong in a guideline
|
|
379
|
+
or context file the rule points at — same split
|
|
380
|
+
[`skill-writing`](../skill-writing/SKILL.md) § Contrastive-example slot uses.
|
|
@@ -530,7 +530,7 @@ tool skill*, **never** a new skill per pitfall and never a generated grid (see
|
|
|
530
530
|
[`size-enforcement`](../../rules/size-enforcement.md) § Per-tool pitfall
|
|
531
531
|
content).
|
|
532
532
|
|
|
533
|
-
## Rationalizations-to-reject section (
|
|
533
|
+
## Rationalizations-to-reject section (recommended pattern, security-stop-routed skills)
|
|
534
534
|
|
|
535
535
|
A skill routed by `security-sensitive-stop` already carries a forbidden-moves or
|
|
536
536
|
failure-mode list. Those describe **what is wrong**. They do not answer the
|
|
@@ -553,7 +553,7 @@ the skip actually happens.
|
|
|
553
553
|
Keep it to the arguments that have really been made. An invented rationalization
|
|
554
554
|
teaches nothing and dilutes the ones that recur.
|
|
555
555
|
|
|
556
|
-
## Non-negotiable-deliverable section (
|
|
556
|
+
## Non-negotiable-deliverable section (recommended pattern, adjacent-technology clusters)
|
|
557
557
|
|
|
558
558
|
Some skills sit next to a technology that is a *tempting wrong answer* — close
|
|
559
559
|
enough to look interchangeable, different enough that picking it silently loses
|
|
@@ -644,6 +644,84 @@ This duplicates the rules on purpose, and the duplication is the point: the
|
|
|
644
644
|
rules are the enforcement, the section is what survives the artifact leaving
|
|
645
645
|
their reach.
|
|
646
646
|
|
|
647
|
+
## Mechanism-teaching section (optional pattern, skills whose subject has one)
|
|
648
|
+
|
|
649
|
+
A procedure tells the agent what to do in the cases you thought of. A
|
|
650
|
+
**mechanism** tells it why those steps work, which is the only thing that
|
|
651
|
+
transfers to the case you did not. Where a skill's subject has a mechanism worth
|
|
652
|
+
one screen — how the cascade resolves, why the queue drops the message, what the
|
|
653
|
+
scheduler actually orders — state it in a `## How it works` section, ahead of the
|
|
654
|
+
procedure that depends on it.
|
|
655
|
+
|
|
656
|
+
Keep it to one screen and to mechanisms only: no history, no alternatives-considered,
|
|
657
|
+
no restating the procedure in prose. If it takes more than a screen, the skill is
|
|
658
|
+
carrying a guideline's worth of material and should point at one instead.
|
|
659
|
+
|
|
660
|
+
The test for whether it earns its place: name a plausible situation the procedure
|
|
661
|
+
does not cover, and check whether this section is enough to answer it. If the
|
|
662
|
+
answer is still "read the source", the section is decoration.
|
|
663
|
+
|
|
664
|
+
Optional, and it gets no gate: "teaches a mechanism" is a prose judgement, and a
|
|
665
|
+
predicate over prose is the false-positive class this file's other patterns avoid.
|
|
666
|
+
|
|
667
|
+
## Illustrative-not-verbatim marker (optional pattern, reference code)
|
|
668
|
+
|
|
669
|
+
A code block in a skill is read as copy-me by default. When a block is teaching a
|
|
670
|
+
*shape* — a signature to mirror, a structure to follow, pseudocode standing in for
|
|
671
|
+
the real call — say so on the line above it:
|
|
672
|
+
|
|
673
|
+
```
|
|
674
|
+
<!-- illustrative: shape only, not a working snippet -->
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
One line, immediately above the fence, in the skill's own words if you prefer.
|
|
678
|
+
What matters is that the reader can tell shape-teaching code from code that runs,
|
|
679
|
+
because the failure mode is silent: an agent pastes the illustration, it almost
|
|
680
|
+
works, and the debugging starts from the wrong premise.
|
|
681
|
+
|
|
682
|
+
The inverse deserves the same care — a block that IS meant to be run verbatim,
|
|
683
|
+
where a reader might assume otherwise, says so too. See also `### 4. Add
|
|
684
|
+
safe/unsafe example`, which governs a different axis: safe-vs-unsafe, not
|
|
685
|
+
runnable-vs-illustrative.
|
|
686
|
+
|
|
687
|
+
## Headline metric + closing report (optional pattern, optimization skills)
|
|
688
|
+
|
|
689
|
+
A skill whose job is to make something better — smaller, faster, cheaper, quieter
|
|
690
|
+
— has to say **which single number it moves**. Without one, "optimized" is a
|
|
691
|
+
claim nobody can check and every run reports success.
|
|
692
|
+
|
|
693
|
+
So an optimization skill names, up front:
|
|
694
|
+
|
|
695
|
+
1. **The headline metric.** One number, with its unit and how it is measured.
|
|
696
|
+
"Initial-context tokens, measured by `audit-tokens`" — not "context cost".
|
|
697
|
+
2. **The closing report shape.** Before, after, delta, and the command that
|
|
698
|
+
produced both. The same command for both readings, or the delta is not a delta.
|
|
699
|
+
|
|
700
|
+
State what the metric does **not** capture, in one line. An optimization that
|
|
701
|
+
moves the headline number by degrading something it does not measure is the
|
|
702
|
+
failure this pattern exists to make visible, and only the author knows where that
|
|
703
|
+
edge is.
|
|
704
|
+
|
|
705
|
+
## Contrastive-example slot (optional pattern, authoring and behaviour skills)
|
|
706
|
+
|
|
707
|
+
Where a skill's guidance is easy to agree with and hard to apply — tone, phrasing,
|
|
708
|
+
question shape, what counts as evidence — a wrong/right pair does what prose
|
|
709
|
+
cannot: it shows the near-miss. Six live corpora already carry them:
|
|
710
|
+
[`direct-answers-demos`](../../../docs/guidelines/agent-infra/direct-answers-demos.md),
|
|
711
|
+
[`asking-and-brevity-examples`](../../../docs/guidelines/agent-infra/asking-and-brevity-examples.md),
|
|
712
|
+
[`language-and-tone-examples`](../../../docs/guidelines/agent-infra/language-and-tone-examples.md),
|
|
713
|
+
and `autonomy-examples` / `interrupt-examples` / `cheap-question-mechanics` under
|
|
714
|
+
`src/agent-src/contexts/execution/`.
|
|
715
|
+
|
|
716
|
+
Follow one of those rather than inventing a format. The shape they share: the
|
|
717
|
+
**wrong** version first, in the form it actually gets written; the **right**
|
|
718
|
+
version second; and one line of **why** — which is the load-bearing part, because
|
|
719
|
+
a pair without it teaches the specific case and not the rule.
|
|
720
|
+
|
|
721
|
+
Put the corpus in a guideline or context file and point at it from the skill when
|
|
722
|
+
it outgrows a section. A near-miss corpus is reference material read on demand,
|
|
723
|
+
not always-loaded prose.
|
|
724
|
+
|
|
647
725
|
## Do NOT
|
|
648
726
|
|
|
649
727
|
* Write documentation-style, pointer-only, or too-broad skills ("Laravel skill", "Django skill")
|
|
@@ -222,10 +222,12 @@ right line beats five minutes of IDE breakpoints.
|
|
|
222
222
|
|
|
223
223
|
**Known failure signature?** If the symptom has a recognisable shape — a
|
|
224
224
|
tool/agent loop, HTTP 429, `ECONNREFUSED`, `ENOENT`, timeout/hang, OOM, flaky
|
|
225
|
-
test,
|
|
225
|
+
test, works-locally-fails-in-CI, or repeated attempts at a tool that is not
|
|
226
|
+
available — consult the symptom → cause → first-check
|
|
226
227
|
lookup in [`failure-signatures`](../../../docs/guidelines/agent-infra/failure-signatures.md)
|
|
227
|
-
to shortcut Phase 2 (Isolate) with the highest-probability first check.
|
|
228
|
-
|
|
228
|
+
to shortcut Phase 2 (Isolate) with the highest-probability first check. Each row
|
|
229
|
+
carries a stable id you can cite, and a discrimination drill that produces its
|
|
230
|
+
documented symptom. It supplements the loop; it does not replace it.
|
|
229
231
|
|
|
230
232
|
## Condition-based waiting (intermittent bugs)
|
|
231
233
|
|
|
@@ -157,8 +157,32 @@ Risks: <arbitrary values, !important, dark-mode gaps>
|
|
|
157
157
|
|
|
158
158
|
When `DESIGN.md` declares `## Taste Dials`, honour them: Variance → layout-family spread + asymmetry tolerance; Motion → animation budget + reduced-motion posture; Density → spacing scale + information-per-viewport. Absent → follow the design brief's inferred dials.
|
|
159
159
|
|
|
160
|
+
## Security constraints
|
|
161
|
+
|
|
162
|
+
`scripts/tailwind_config_gen.ts` is the only shipped script.
|
|
163
|
+
|
|
164
|
+
- **What it may touch** — exactly one file: `--output PATH`, or
|
|
165
|
+
`tailwind.config.ts` / `.js` in the current working directory when no
|
|
166
|
+
`--output` is given. It reads nothing from the project.
|
|
167
|
+
- **What it must never do** — write anywhere other than that one path,
|
|
168
|
+
reach the network, or spawn a subprocess. It does none of these.
|
|
169
|
+
- **Default invocation** — **mutating**, and stated here because it is a
|
|
170
|
+
real exception rather than a comfortable one: a bare invocation writes
|
|
171
|
+
`tailwind.config.ts` into `cwd`, overwriting an existing config without
|
|
172
|
+
prompting. `--validate-only` is the read-only path — it prints the
|
|
173
|
+
generated config to stdout and writes nothing. Run `--validate-only`
|
|
174
|
+
first, and pass `--output` when the target is not the cwd default.
|
|
175
|
+
This default contradicts `skill-writing`'s "never mutate on a bare
|
|
176
|
+
invocation" line; it is recorded rather than silently tolerated, and
|
|
177
|
+
changing the script's default is a caller-visible behaviour change that
|
|
178
|
+
belongs in its own change, not here.
|
|
179
|
+
- **Outbound** — nothing. No network access.
|
|
180
|
+
|
|
160
181
|
## Do NOT
|
|
161
182
|
|
|
183
|
+
- Do NOT run `tailwind_config_gen` bare in a project that already has a
|
|
184
|
+
`tailwind.config.*` — it overwrites without asking. Use
|
|
185
|
+
`--validate-only` first, then `--output` at the intended path.
|
|
162
186
|
- Do NOT add `!important` to win a specificity fight; restructure
|
|
163
187
|
the cascade or extract the conflicting style.
|
|
164
188
|
- Do NOT introduce a new colour outside `tailwind.config` without
|
|
@@ -163,6 +163,27 @@ expect([403, 404]).toContain((await api.get(`/users/${otherTenantUser.id}`)).sta
|
|
|
163
163
|
|
|
164
164
|
Deriving from seeded data is necessary but **not sufficient** — a test that only reads back the exact fields it seeded is still overfit (a mutation in the code survives it). A real test also exercises boundary (empty / null / max / Unicode), error (missing / invalid), and, on security-sensitive paths, an abuse case (IDOR, injection string, XSS payload). Coverage of *cases*, not just lines.
|
|
165
165
|
|
|
166
|
+
**The adversarial variant, and it is the one that looks safest.** A *negative* test — the abuse case just named — fails the same way, but nothing about it reads as weak: it names an attack, it asserts a refusal, it goes green. It passes because **something other than the control under test** produced that refusal. Two shapes, both from one real review:
|
|
167
|
+
|
|
168
|
+
- **The hostile value sits only in the argument.** A guard rejecting a path-traversing `token` was pinned by planting a fixture whose own `token` field held the *legitimate* value while `'../outside'` was passed as the argument. Downstream, an ordinary token comparison mismatched and returned exactly the expected refusal — delete the guard and the test still passes. The hostile value belongs in the **field under test**, not only in the call.
|
|
169
|
+
- **The assertion cannot fail.** The same test asserted the planted file still existed after a rejected claim. The two directories were siblings, so both `../x.json` joins resolved to one path and the rename could never have moved it — a green no implementation could turn red.
|
|
170
|
+
|
|
171
|
+
Gate:
|
|
172
|
+
|
|
173
|
+
```
|
|
174
|
+
BEFORE trusting a negative test:
|
|
175
|
+
Delete or invert the control it claims to pin. Does it FAIL?
|
|
176
|
+
RESTORE THE CONTROL IMMEDIATELY — before any other edit, before any commit.
|
|
177
|
+
The mutation is a probe, never a change. A deleted guard left deleted is
|
|
178
|
+
a shipped vulnerability produced by a test-quality check.
|
|
179
|
+
IF it still passes: it is pinning something else. Name what, then fix the test.
|
|
180
|
+
IF an assertion cannot fail for ANY implementation: delete THE ASSERTION —
|
|
181
|
+
it is decoration. Only after proving vacuity, and never to turn a red test
|
|
182
|
+
green — that is Anti-Pattern 7 below.
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
No mutation-testing rig required: comment the control out, run that one spec, put it back. A negative test nobody has ever watched fail is a claim, not evidence.
|
|
186
|
+
|
|
166
187
|
### Anti-Pattern 7 — Gaming the green (test-integrity)
|
|
167
188
|
|
|
168
189
|
Symptom: a test fails, and the "fix" edits the *test* instead of the code — `.skip` / `it.skip` / `xfail` / `@Disabled` on the failing case, deleting the failing assertion, loosening `toBe(x)` to `toBeTruthy()`, or rewriting the `expected` value to whatever the (possibly buggy) code now emits. The suite goes green while the behavior stays broken — the worst outcome, because green now *hides* the bug. This is the automation-bias trap: under pressure to finish, the agent optimizes the signal (green) instead of the target (correct behavior).
|
|
@@ -211,6 +232,7 @@ This is the test-surface instance of the `autonomous-execution` N=3 / allowlist-
|
|
|
211
232
|
- Do NOT mark a story complete until at least one test was watched failing first.
|
|
212
233
|
- Do NOT hardcode an expected value the code will emit, or pin a regex to an incidental fixed date/id — derive from inputs and seeded data, and vary the input across cases.
|
|
213
234
|
- Do NOT skip / `xfail` / delete a failing test, or rewrite its `expected` to the code's current output, to get a green run — fix the code, or state a real spec change in the diff.
|
|
235
|
+
- Do NOT trust a negative test you have never watched fail — delete the control it pins, confirm the test goes red, restore the control immediately.
|
|
214
236
|
|
|
215
237
|
## Auto-trigger keywords
|
|
216
238
|
|
|
@@ -225,6 +247,9 @@ This is the test-surface instance of the `autonomous-execution` N=3 / allowlist-
|
|
|
225
247
|
- gaming the green
|
|
226
248
|
- skip failing test
|
|
227
249
|
- test integrity
|
|
250
|
+
- negative test
|
|
251
|
+
- vacuous assertion
|
|
252
|
+
- abuse-case test
|
|
228
253
|
|
|
229
254
|
## Provenance
|
|
230
255
|
|
|
@@ -39,7 +39,7 @@ schema_version: 1
|
|
|
39
39
|
# CI guard: a release bump of `package.json` must update this value
|
|
40
40
|
# in lockstep — see scripts/check_template_pin_drift.ts (road-to-
|
|
41
41
|
# portable-runtime-and-update-check P3.3).
|
|
42
|
-
agent_config_version: "9.
|
|
42
|
+
agent_config_version: "9.35.0"
|
|
43
43
|
|
|
44
44
|
# --- Project identity ---
|
|
45
45
|
project:
|
|
@@ -388,8 +388,7 @@ function* _iter_knowledge_entries(): Generator<[string, Record<string, unknown>]
|
|
|
388
388
|
/**
|
|
389
389
|
* Naive relevance score: max over keys of (glob-match | substring).
|
|
390
390
|
*
|
|
391
|
-
*
|
|
392
|
-
* The `present` path returns a real score from agent-memory.
|
|
391
|
+
* Retrieval is best-effort by design — this is the only scoring path.
|
|
393
392
|
*/
|
|
394
393
|
export function _score(entry: Record<string, unknown>, keys: string[]): number {
|
|
395
394
|
if (keys.length === 0) {
|
|
@@ -214,10 +214,8 @@ export interface SignalRecord {
|
|
|
214
214
|
/**
|
|
215
215
|
* Append a signal entry. Returns the written record, or null when skipped.
|
|
216
216
|
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
* Phase 3. For now, the file path is the single source of truth so
|
|
220
|
-
* merge-safety is preserved in every mode.
|
|
217
|
+
* The file path is the single source of truth: there is no external backend
|
|
218
|
+
* to route to, so merge-safety holds in every mode.
|
|
221
219
|
*/
|
|
222
220
|
export function emit(
|
|
223
221
|
entry_type: string,
|
|
@@ -41,6 +41,22 @@ type Any = unknown;
|
|
|
41
41
|
|
|
42
42
|
const _BLOCK_REASON_PREFIX = 'decision_gate';
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Stage an action for confirmation and return the surface lines that name it.
|
|
46
|
+
*
|
|
47
|
+
* An injected collaborator rather than a direct import of
|
|
48
|
+
* `staged_confirmation_store.js`, for two reasons. The gate is called from a
|
|
49
|
+
* hook bus with no repo root and no clock of its own, and — the load-bearing
|
|
50
|
+
* one — an absent stager keeps the `ask` path byte-identical to what ships
|
|
51
|
+
* today. The confirmation primitive is deliberately UNBOUND until blocker
|
|
52
|
+
* `confirmation-degraded-host-semantics` is decided (5 of 8 hosts have no
|
|
53
|
+
* `pre_tool_use` slot), so reaching it has to be an explicit opt-in and not a
|
|
54
|
+
* behaviour change that arrives with an upgrade.
|
|
55
|
+
*/
|
|
56
|
+
export interface ConfirmationStager {
|
|
57
|
+
stage(input: { action: string; object: string; source: string }): readonly string[];
|
|
58
|
+
}
|
|
59
|
+
|
|
44
60
|
/**
|
|
45
61
|
* Evaluate decision-engine gates on every `AFTER_STEP`.
|
|
46
62
|
*
|
|
@@ -49,9 +65,11 @@ const _BLOCK_REASON_PREFIX = 'decision_gate';
|
|
|
49
65
|
*/
|
|
50
66
|
export class DecisionGateHook {
|
|
51
67
|
private readonly _settings: DecisionEngineSettings;
|
|
68
|
+
private readonly _stager: ConfirmationStager | null;
|
|
52
69
|
|
|
53
|
-
constructor(settings: DecisionEngineSettings) {
|
|
70
|
+
constructor(settings: DecisionEngineSettings, stager: ConfirmationStager | null = null) {
|
|
54
71
|
this._settings = settings;
|
|
72
|
+
this._stager = stager;
|
|
55
73
|
}
|
|
56
74
|
|
|
57
75
|
/** Register the gate callback on `AFTER_STEP`. */
|
|
@@ -109,10 +127,22 @@ export class DecisionGateHook {
|
|
|
109
127
|
DecisionGateHook._surface(decision, 'ask_timeout'),
|
|
110
128
|
);
|
|
111
129
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
130
|
+
// `stop` and `ask` share this exit: both halt, which is the policy —
|
|
131
|
+
// ask and action are strictly sequential, so an interactive gate does
|
|
132
|
+
// not act while it asks. What `ask` adds, and only when a stager is
|
|
133
|
+
// injected, is a token on the surface so the answer can be matched to
|
|
134
|
+
// THIS question and consumed exactly once. The halt reason is
|
|
135
|
+
// deliberately unchanged: it is the identity downstream readers match on.
|
|
136
|
+
let surface = DecisionGateHook._surface(decision);
|
|
137
|
+
if (action === 'ask' && this._stager !== null) {
|
|
138
|
+
const extra = this._stager.stage({
|
|
139
|
+
action: `work_engine:advance:${decision.gate_id}`,
|
|
140
|
+
object: `phase ${decision.phase}`,
|
|
141
|
+
source: _BLOCK_REASON_PREFIX,
|
|
142
|
+
});
|
|
143
|
+
surface = [...surface, '', ...extra];
|
|
144
|
+
}
|
|
145
|
+
throw new HookHalt(`${_BLOCK_REASON_PREFIX}:${decision.gate_id}`, surface);
|
|
116
146
|
}
|
|
117
147
|
|
|
118
148
|
// -- formatting helpers -------------------------------------------
|