refactor(skills): retire nc:env-sync — the data/env/env mirror is dead

Upstream removed every setup-side writer of data/env/env (c82f062d) because
nothing has read it since the container mount was dropped — before this
branch even forked. Our engine codified the dead pattern as a directive:
every apply copied the full .env (live tokens included) into a file nothing
consumes.

- engine: env-sync handler removed from selfStatus + applyOne
- grammar: dropped from KNOWN; a new RETIRED table gives a targeted lint
  error ("delete the fence, the adapter reads .env directly") instead of a
  generic unknown-directive message
- skills: fence stripped from the 14 converted skills; orphaned "sync to the
  container" prose cleaned (incl. add-deltachat/add-wechat old-format prose
  upstream's sweep missed); teams troubleshooting entry rewritten
- policy/spec doc lists updated

Suite 827 passed | 1 skipped; all nc: skills lint clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
gavrielc
2026-07-05 08:27:40 +03:00
parent 81fea1d7ec
commit 40b65521c0
21 changed files with 27 additions and 70 deletions
-6
View File
@@ -142,8 +142,6 @@ function selfStatus(d: Directive, root: string): { status: StepStatus; detail: s
? { status: 'apply', detail: `set ${missing.join(', ')} in .env` }
: { status: 'skip', detail: `${keys.join(', ')} already set` };
}
case 'env-sync':
return { status: 'apply', detail: 'sync .env → data/env/env' };
case 'json-merge': {
const into = String(d.attrs.into ?? '');
const key = String(d.attrs.key ?? '');
@@ -668,10 +666,6 @@ async function applyOne(
}
break;
}
case 'env-sync':
mkdirSync(join(root, 'data/env'), { recursive: true });
copyFileSync(join(root, '.env'), join(root, 'data/env/env'));
break;
case 'json-merge': {
const into = String(d.attrs.into);
const key = String(d.attrs.key);
+9 -1
View File
@@ -19,7 +19,6 @@ describe('skill-directives parser, on the converted add-slack', () => {
'prompt', // credentials: capture bot token
'prompt', // credentials: capture signing secret
'env-set', // credentials: write captured values to .env
'env-sync', // credentials: sync to container
'operator', // credentials: event-delivery walkthrough
'prompt', // resolve: owner member id (owner_handle)
'run', // resolve: validate token (auth.test) — fast-fail before the restart
@@ -187,6 +186,15 @@ describe('append at:<marker> attribute', () => {
});
});
describe('retired directives', () => {
it('flags nc:env-sync with a targeted retirement error, not a generic unknown', () => {
const probs = validate(parseDirectives(['```nc:env-sync', '```'].join('\n')));
expect(probs).toHaveLength(1);
expect(probs[0].message).toMatch(/retired/);
expect(probs[0].message).toMatch(/data\/env\/env/);
});
});
describe('when: guard + multi-field capture', () => {
it('parses when: into attrs and lints a guard whose var an earlier prompt defined', () => {
const md = ['```nc:prompt mode', 'local or remote', '```', '```nc:prompt server_url when:mode=remote', 'url', '```'].join('\n');
+8 -3
View File
@@ -68,7 +68,6 @@
// pauses for confirmation before the next side effect is derived from
// document structure (scripts/skill-policy.ts), never authored here.
// env-set body: `KEY=value` ({{var}} allowed) set-if-absent
// env-sync (no body) `.env` → data/env/env idempotent copy
// json-merge into:<file> key:<field> body: a JSON object push-if-absent
//
// `append` without `at:` adds to EOF; with `at:<marker>` it inserts before the
@@ -113,7 +112,12 @@ export interface Problem {
const FENCE = /^```(\S.*)?$/;
const EXACT_SEMVER = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;
const VAR_REF = /\{\{\s*([A-Za-z_][A-Za-z0-9_]*)\s*\}\}/g;
const KNOWN = new Set(['copy', 'append', 'dep', 'run', 'prompt', 'operator', 'env-set', 'env-sync', 'json-merge']);
const KNOWN = new Set(['copy', 'append', 'dep', 'run', 'prompt', 'operator', 'env-set', 'json-merge']);
// Retired directives get a targeted lint error (not just "unknown") so an
// author knows the removal was deliberate and what to do instead.
const RETIRED: Record<string, string> = {
'env-sync': 'nc:env-sync was retired — nothing reads the data/env/env mirror (and it copied live tokens); delete the fence, the adapter reads .env directly',
};
const PROMPT_FLAGS = new Set(['secret']);
export function parseDirectives(markdown: string): Directive[] {
@@ -203,7 +207,8 @@ export function validate(directives: Directive[], ctx?: { chatVersion?: string }
const defined = new Set<string>();
const flag = (d: Directive, message: string) => problems.push({ line: d.line, kind: d.kind, message });
for (const d of directives) {
if (!KNOWN.has(d.kind)) flag(d, `unknown directive nc:${d.kind}`);
if (RETIRED[d.kind]) flag(d, RETIRED[d.kind]);
else if (!KNOWN.has(d.kind)) flag(d, `unknown directive nc:${d.kind}`);
switch (d.kind) {
case 'dep':
for (const spec of d.body) {
+1 -1
View File
@@ -65,7 +65,7 @@ function guardOf(d: Directive): { v: string; value: string } | undefined {
* 3. Next compatible directive is a `prompt` no confirm (the prompt is the
* barrier the human can't paste a token before doing the steps).
* 4. No such directive (end of document) no confirm (a final handoff block).
* 5. Anything else (`run`, `copy`, `dep`, `append`, `env-set`, `env-sync`,
* 5. Anything else (`run`, `copy`, `dep`, `append`, `env-set`,
* `json-merge`) confirm, with the flavor derived from that barrier
* directive's effect (`effect:step` readiness, else completed).
*