@1aboveio/skills 0.13.0 → 0.15.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/README.md CHANGED
@@ -24,7 +24,7 @@ Run the reviewed shell prerequisites separately when you need their capabilities
24
24
  #### Matt Pocock Toolkit
25
25
 
26
26
  ```sh
27
- SKILLS_DOWNLOAD_MAX_BYTES=33554432 SKILLS_EXTRACT_MAX_FILES=4096 SKILLS_EXTRACT_MAX_BYTES=67108864 npx skills@1.5.22 add https://codeload.github.com/mattpocock/skills/tar.gz/ed37663cc5fbef691ddfecd080dff42f7e7e350d --global --agent universal claude-code --skill batch-grill-me code-review codebase-design diagnosing-bugs domain-modeling grill-me grill-with-docs grilling handoff improve-codebase-architecture prototype research resolving-merge-conflicts setup-matt-pocock-skills tdd to-spec to-tickets triage wayfinder
27
+ SKILLS_DOWNLOAD_MAX_BYTES=33554432 SKILLS_EXTRACT_MAX_FILES=4096 SKILLS_EXTRACT_MAX_BYTES=67108864 npx skills@1.5.22 add https://codeload.github.com/mattpocock/skills/tar.gz/ed37663cc5fbef691ddfecd080dff42f7e7e350d --global --agent universal claude-code --skill batch-grill-me code-review codebase-design diagnosing-bugs domain-modeling grill-me grill-with-docs grilling handoff improve-codebase-architecture prototype research resolving-merge-conflicts setup-matt-pocock-skills tdd to-spec to-tickets triage wayfinder --yes
28
28
  ```
29
29
 
30
30
  #### Impeccable
@@ -51,15 +51,17 @@ Running `install` with no `--group` opens the interactive bundle picker; `update
51
51
  - `--group <id>` — select one group; repeat the flag, or pass a comma-separated list, for several.
52
52
  - `--all` — select every group in the catalog below. Mutually exclusive with `--group`.
53
53
  - `--dry-run` — print the planned buckets and native commands, in order, and run none of them.
54
- - `--yes` — answer the native CLI's prompts, including the overwrite prompt, with yes, and accept
55
- every newly declared group member without prompting.
54
+ - `--yes` — accept every newly declared group member and use native symlink deployment without
55
+ coordinator prompts.
56
+ - `--enable-tdd` — keep TDD installed and allow implicit/model invocation (the default).
57
+ - `--disable-tdd` — keep TDD installed but allow only explicit user invocation.
56
58
 
57
59
  Selecting several groups (interactive picker, repeated `--group`, or `--all`) installs their
58
60
  members as one native `skills add` per skill from the unpacked package, so one skill's failure is
59
- reported by name and never aborts the rest of the selection.
60
-
61
- Without `--yes`, an existing same-named skill produces the native overwrite summary and a prompt.
62
- Declining leaves the existing content and its links exactly as they were.
61
+ reported by name and never aborts the rest of the selection. Every native add uses `--yes` because
62
+ `skills@1.5.22` has no independent `--symlink` flag: this selects the default canonical store plus
63
+ per-harness symlinks without an installation-method prompt, and also confirms replacement of each
64
+ planned same-named skill. The coordinator still owns group and new-member consent before planning.
63
65
 
64
66
  ## Groups in this package
65
67
 
@@ -82,7 +84,7 @@ is an error — the coordinator never infers groups from the filesystem — and
82
84
  no longer selectable are dropped with a warning.
83
85
 
84
86
  An accepted skill whose installed content already matches this package version's published digest is
85
- skipped entirely: no native add, no overwrite prompt. When everything remembered is unchanged and no
87
+ skipped entirely: no native add and no replacement. When everything remembered is unchanged and no
86
88
  new skill is accepted, the run exits 0 as an explicit no-op. `--dry-run` prints the unchanged,
87
89
  would-refresh, and would-add buckets without writing state or invoking the native CLI.
88
90
 
@@ -95,8 +97,8 @@ so the next update asks again.
95
97
 
96
98
  Install and update are best-effort per skill: they continue after a single skill failure, summarize
97
99
  every skill as succeeded, unchanged, declined, or failed, and exit non-zero if anything failed. There
98
- is no rollback or transactional group update; the native CLI remains the sole write and
99
- overwrite-consent authority. Selection memory follows the same per-skill rule: it is rewritten when
100
+ is no rollback or transactional group update; the native CLI remains the sole filesystem write
101
+ authority. Selection memory follows the same per-skill rule: it is rewritten when
100
102
  at least one user-facing member succeeded or was already unchanged, holding exactly those members —
101
103
  failed and declined members are omitted — and a run with no user-facing success leaves the prior
102
104
  file untouched.
@@ -127,7 +129,7 @@ Removing a group leaves its dependency groups and every unrelated skill installe
127
129
  ## Lifecycle
128
130
 
129
131
  `skills@1.5.22` is the only lifecycle authority for this package. The package
130
- plans first-party commands; the native CLI owns installation, linking, overwrite consent, update
132
+ plans first-party commands; the native CLI owns installation, linking, replacement, update
131
133
  and removal. External prerequisites have separate upstream lifecycle and are never package steps.
132
134
 
133
135
  ## Licensing
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1aboveio/skills",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
4
4
  "description": "Install the 1AboveIO first-party skill groups through the native Skills CLI.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -8,7 +8,7 @@
8
8
  "id": "first-party",
9
9
  "type": "first-party",
10
10
  "package": "@1aboveio/skills",
11
- "version": "0.13.0",
11
+ "version": "0.15.0",
12
12
  "updatePolicy": "pinned-npm-version",
13
13
  "members": [
14
14
  {
@@ -184,7 +184,7 @@
184
184
  "sourceId": "first-party",
185
185
  "sourceType": "first-party",
186
186
  "package": "@1aboveio/skills",
187
- "version": "0.13.0",
187
+ "version": "0.15.0",
188
188
  "installPath": null,
189
189
  "members": [
190
190
  "harness-runtime",
@@ -200,7 +200,7 @@
200
200
  "commands": [
201
201
  {
202
202
  "transport": "local",
203
- "command": "npx skills@1.5.22 add . --global --agent universal claude-code --skill harness-runtime e2e-test ensure-coverage implement-and-pr resolve-issues resolve-release review-pr smoke engineering-runtime && npm ci --ignore-scripts --prefix \"$HOME/.agents/skills/engineering-runtime/scripts\""
203
+ "command": "npx skills@1.5.22 add . --global --agent universal claude-code --skill harness-runtime e2e-test ensure-coverage implement-and-pr resolve-issues resolve-release review-pr smoke engineering-runtime && npm --prefix \"$(realpath \"$HOME/.agents/skills/engineering-runtime/scripts\")\" ci --ignore-scripts"
204
204
  }
205
205
  ],
206
206
  "onFailure": {
@@ -218,7 +218,7 @@
218
218
  "sourceId": "first-party",
219
219
  "sourceType": "first-party",
220
220
  "package": "@1aboveio/skills",
221
- "version": "0.13.0",
221
+ "version": "0.15.0",
222
222
  "installPath": null,
223
223
  "members": [
224
224
  "harness-runtime",
@@ -234,7 +234,7 @@
234
234
  "commands": [
235
235
  {
236
236
  "transport": "local",
237
- "command": "npx skills@1.5.22 add . --global --agent universal claude-code --skill harness-runtime e2e-test ensure-coverage implement-and-pr resolve-issues resolve-release review-pr smoke engineering-runtime && npm ci --ignore-scripts --prefix \"$HOME/.agents/skills/engineering-runtime/scripts\""
237
+ "command": "npx skills@1.5.22 add . --global --agent universal claude-code --skill harness-runtime e2e-test ensure-coverage implement-and-pr resolve-issues resolve-release review-pr smoke engineering-runtime && npm --prefix \"$(realpath \"$HOME/.agents/skills/engineering-runtime/scripts\")\" ci --ignore-scripts"
238
238
  }
239
239
  ],
240
240
  "onFailure": {
@@ -262,7 +262,7 @@
262
262
  "sourceId": "first-party",
263
263
  "sourceType": "first-party",
264
264
  "package": "@1aboveio/skills",
265
- "version": "0.13.0",
265
+ "version": "0.15.0",
266
266
  "installPath": null,
267
267
  "members": [
268
268
  "harness-runtime",
@@ -295,7 +295,7 @@
295
295
  "sourceId": "first-party",
296
296
  "sourceType": "first-party",
297
297
  "package": "@1aboveio/skills",
298
- "version": "0.13.0",
298
+ "version": "0.15.0",
299
299
  "installPath": null,
300
300
  "members": [
301
301
  "harness-runtime",
@@ -338,7 +338,7 @@
338
338
  "sourceId": "first-party",
339
339
  "sourceType": "first-party",
340
340
  "package": "@1aboveio/skills",
341
- "version": "0.13.0",
341
+ "version": "0.15.0",
342
342
  "installPath": null,
343
343
  "members": [
344
344
  "harness-runtime",
@@ -368,7 +368,7 @@
368
368
  "sourceId": "first-party",
369
369
  "sourceType": "first-party",
370
370
  "package": "@1aboveio/skills",
371
- "version": "0.13.0",
371
+ "version": "0.15.0",
372
372
  "installPath": null,
373
373
  "members": [
374
374
  "harness-runtime",
@@ -408,7 +408,7 @@
408
408
  "sourceId": "first-party",
409
409
  "sourceType": "first-party",
410
410
  "package": "@1aboveio/skills",
411
- "version": "0.13.0",
411
+ "version": "0.15.0",
412
412
  "installPath": null,
413
413
  "members": [
414
414
  "harness-runtime",
@@ -440,7 +440,7 @@
440
440
  "sourceId": "first-party",
441
441
  "sourceType": "first-party",
442
442
  "package": "@1aboveio/skills",
443
- "version": "0.13.0",
443
+ "version": "0.15.0",
444
444
  "installPath": null,
445
445
  "members": [
446
446
  "harness-runtime",
@@ -480,7 +480,7 @@
480
480
  "sourceId": "first-party",
481
481
  "sourceType": "first-party",
482
482
  "package": "@1aboveio/skills",
483
- "version": "0.13.0",
483
+ "version": "0.15.0",
484
484
  "installPath": null,
485
485
  "members": [
486
486
  "harness-runtime"
@@ -506,7 +506,7 @@
506
506
  "sourceId": "first-party",
507
507
  "sourceType": "first-party",
508
508
  "package": "@1aboveio/skills",
509
- "version": "0.13.0",
509
+ "version": "0.15.0",
510
510
  "installPath": null,
511
511
  "members": [
512
512
  "harness-runtime"
@@ -538,7 +538,7 @@
538
538
  "sourceId": "first-party",
539
539
  "sourceType": "first-party",
540
540
  "package": "@1aboveio/skills",
541
- "version": "0.13.0",
541
+ "version": "0.15.0",
542
542
  "installPath": null,
543
543
  "members": [
544
544
  "harness-runtime",
@@ -571,7 +571,7 @@
571
571
  "commands": [
572
572
  {
573
573
  "transport": "local",
574
- "command": "npx skills@1.5.22 add . --global --agent universal claude-code --skill harness-runtime e2e-test ensure-coverage implement-and-pr resolve-issues resolve-release review-pr smoke engineering-runtime cloud-build cloud-debug cloud-deploy docker google-cloud mergify podman airflow-dag-develop app-debug pyspark python-backend better-auth monorepo nextjs-fullstack prisma-setup shadcn zod-v4 && npm ci --ignore-scripts --prefix \"$HOME/.agents/skills/engineering-runtime/scripts\""
574
+ "command": "npx skills@1.5.22 add . --global --agent universal claude-code --skill harness-runtime e2e-test ensure-coverage implement-and-pr resolve-issues resolve-release review-pr smoke engineering-runtime cloud-build cloud-debug cloud-deploy docker google-cloud mergify podman airflow-dag-develop app-debug pyspark python-backend better-auth monorepo nextjs-fullstack prisma-setup shadcn zod-v4 && npm --prefix \"$(realpath \"$HOME/.agents/skills/engineering-runtime/scripts\")\" ci --ignore-scripts"
575
575
  }
576
576
  ],
577
577
  "onFailure": {
@@ -589,7 +589,7 @@
589
589
  "sourceId": "first-party",
590
590
  "sourceType": "first-party",
591
591
  "package": "@1aboveio/skills",
592
- "version": "0.13.0",
592
+ "version": "0.15.0",
593
593
  "installPath": null,
594
594
  "members": [
595
595
  "harness-runtime",
@@ -622,7 +622,7 @@
622
622
  "commands": [
623
623
  {
624
624
  "transport": "local",
625
- "command": "npx skills@1.5.22 add . --global --agent universal claude-code --skill harness-runtime e2e-test ensure-coverage implement-and-pr resolve-issues resolve-release review-pr smoke engineering-runtime cloud-build cloud-debug cloud-deploy docker google-cloud mergify podman airflow-dag-develop app-debug pyspark python-backend better-auth monorepo nextjs-fullstack prisma-setup shadcn zod-v4 && npm ci --ignore-scripts --prefix \"$HOME/.agents/skills/engineering-runtime/scripts\""
625
+ "command": "npx skills@1.5.22 add . --global --agent universal claude-code --skill harness-runtime e2e-test ensure-coverage implement-and-pr resolve-issues resolve-release review-pr smoke engineering-runtime cloud-build cloud-debug cloud-deploy docker google-cloud mergify podman airflow-dag-develop app-debug pyspark python-backend better-auth monorepo nextjs-fullstack prisma-setup shadcn zod-v4 && npm --prefix \"$(realpath \"$HOME/.agents/skills/engineering-runtime/scripts\")\" ci --ignore-scripts"
626
626
  }
627
627
  ],
628
628
  "onFailure": {
@@ -7,6 +7,10 @@ import { emitKeypressEvents } from 'node:readline';
7
7
  import { dirname, join, resolve } from 'node:path';
8
8
  import { fileURLToPath } from 'node:url';
9
9
 
10
+ import {
11
+ applyInvocationPolicy,
12
+ reviewedSourceInjections,
13
+ } from '../../engineering/engineering-runtime/scripts/invocation-policy.mjs';
10
14
  import { isMainModule } from '../../engineering/engineering-runtime/scripts/main-module.mjs';
11
15
  // The digest authority for the update skip is the ONE existing predicate pair: contentDigest with
12
16
  // the published-content scope for first-party members, exact-tree for external ones — the same
@@ -80,6 +84,9 @@ export function loadSelectionState(env = process.env) {
80
84
  if (!Array.isArray(parsed.groups)) {
81
85
  throw selectionError(path, 'is invalid: groups must be an array');
82
86
  }
87
+ if (parsed.tddImplicitInvocation !== undefined && typeof parsed.tddImplicitInvocation !== 'boolean') {
88
+ throw selectionError(path, 'is invalid: tddImplicitInvocation must be a boolean');
89
+ }
83
90
  for (const group of parsed.groups) {
84
91
  const valid = group
85
92
  && typeof group === 'object'
@@ -132,7 +139,14 @@ function rememberedGroupIds(state, catalog, stderr) {
132
139
  // declined is dropped entirely, so the next no-arg update never replays a group this run did not
133
140
  // actually deliver. The transport label is observational only — both render targets share this
134
141
  // file.
135
- export function selectionForRun(action, groupIds, { renderTarget, now = new Date(), succeededMembers, omitMembers, ...transport } = {}) {
142
+ export function selectionForRun(action, groupIds, {
143
+ renderTarget,
144
+ now = new Date(),
145
+ succeededMembers,
146
+ omitMembers,
147
+ tddImplicitInvocation = true,
148
+ ...transport
149
+ } = {}) {
136
150
  const omitted = new Set(omitMembers ?? []);
137
151
  const recipes = readRecipes(resolveBundleTransport(transport).recipesPath);
138
152
  const groups = new Map(recipes.groups.map((group) => [group.id, group]));
@@ -141,6 +155,7 @@ export function selectionForRun(action, groupIds, { renderTarget, now = new Date
141
155
  schemaVersion: SELECTION_SCHEMA_VERSION,
142
156
  updatedAt: now.toISOString(),
143
157
  transport: { renderTarget },
158
+ tddImplicitInvocation,
144
159
  groups: groupIds
145
160
  .map((groupId) => {
146
161
  const group = groups.get(groupId);
@@ -274,9 +289,9 @@ function localFirstPartyCommand(command, localRoot, groupId) {
274
289
  // adds let the run record exactly which skill failed, continue with the remaining skills, and keep
275
290
  // only the succeeded skills in selection memory. Recipe step/member order is preserved
276
291
  // (dependency-first), and a member shared by several selected groups (harness-runtime) is still
277
- // planned exactly once, at its first occurrence. Native `skills add` prompts once per invocation
278
- // for the installation method, so per-skill execution relies on the native CLI's own non-interactive
279
- // handling; what the coordinator must guarantee is attribution, ordering and deduplication.
292
+ // planned exactly once, at its first occurrence. The pinned native CLI has no `--symlink` selector,
293
+ // so the coordinator adds `--yes` to choose its default symlink deployment without one installation-
294
+ // method prompt per skill. Attribution, ordering and deduplication remain coordinator-owned.
280
295
  const SKILL_TAIL = /^(.*? --skill )(.+?)( && .*)?$/;
281
296
 
282
297
  // The generated step binds its runtime suffix (` && npm ci ...`) to the whole coalesced command.
@@ -303,7 +318,12 @@ function withYesFlag(command) {
303
318
  return `${match[1]}${match[2]} --yes${match[3] || ''}`;
304
319
  }
305
320
 
306
- export function planBundleCommands(action, groupIds, { yes = false, skipMembers, omitMembers, ...transport } = {}) {
321
+ export function planBundleCommands(action, groupIds, {
322
+ enableTdd = true,
323
+ skipMembers,
324
+ omitMembers,
325
+ ...transport
326
+ } = {}) {
307
327
  if (!ACTIONS.has(action)) throw new Error(`Unsupported bundle action: ${action}`);
308
328
  // Members the user declined this run (#1128) are filtered out of every native --skill list.
309
329
  // Support dependencies (harness-runtime) are never omittable: they are silent install
@@ -352,13 +372,19 @@ export function planBundleCommands(action, groupIds, { yes = false, skipMembers,
352
372
  }
353
373
  let skillCommand = withSkills(bare, [skill]);
354
374
  if (suffix && skill === RUNTIME_MEMBER) skillCommand = `${skillCommand}${suffix}`;
355
- if (yes) skillCommand = withYesFlag(skillCommand);
375
+ // The pinned CLI has no --symlink flag. --yes is its only supported way to select the
376
+ // default symlink deployment without presenting the install-method chooser.
377
+ skillCommand = withYesFlag(skillCommand);
378
+ const invocationPolicy = skill === 'tdd' && enableTdd
379
+ ? undefined
380
+ : step.invocationPolicies?.find((candidate) => candidate.installName === skill)?.policy;
356
381
  const entry = {
357
382
  member: skill,
358
383
  groupIds: [groupId],
359
384
  groupId,
360
385
  groupName: group.displayName,
361
386
  sourceId: step.sourceId,
387
+ ...(invocationPolicy ? { invocationPolicy } : {}),
362
388
  command: skillCommand,
363
389
  cwd: localRoot,
364
390
  failureMessage: step.onFailure.message,
@@ -378,6 +404,8 @@ export function planBundleCommands(action, groupIds, { yes = false, skipMembers,
378
404
  // exit status, and selection persistence sees exactly which members succeeded.
379
405
  export function runBundleCommands(commands, {
380
406
  execute = (command, cwd) => spawnSync('/bin/bash', ['-lc', command], { cwd, stdio: 'inherit' }),
407
+ applyPolicy = applyInvocationPolicy,
408
+ skillsRoot,
381
409
  stdout = process.stdout,
382
410
  stderr = process.stderr,
383
411
  } = {}) {
@@ -387,8 +415,21 @@ export function runBundleCommands(commands, {
387
415
  stdout.write(`\nInstalling ${planned.member} (${planned.sourceId})...\n`);
388
416
  const result = execute(planned.command, planned.cwd);
389
417
  if (result.status === 0) {
390
- succeeded.push(planned);
391
- continue;
418
+ try {
419
+ if (planned.invocationPolicy) {
420
+ applyPolicy({
421
+ installName: planned.member,
422
+ policy: planned.invocationPolicy,
423
+ ...(skillsRoot ? { skillsRoot } : {}),
424
+ });
425
+ }
426
+ succeeded.push(planned);
427
+ continue;
428
+ } catch (error) {
429
+ stderr.write(`Invocation policy failed for ${planned.member}: ${error.message}\n`);
430
+ failed.push({ planned, status: 1 });
431
+ continue;
432
+ }
392
433
  }
393
434
  stderr.write(`${planned.failureMessage}\n`);
394
435
  failed.push({ planned, status: Number.isInteger(result.status) ? result.status : 1 });
@@ -414,10 +455,9 @@ function printRunSummary(stdout, { succeeded, failed, unchanged = [], declined =
414
455
  // --- Digest classification (epic #1124 / issue #1127) --------------------------------------------
415
456
  //
416
457
  // An `update` that re-adds a skill whose installed content already digests to what this invocation
417
- // would install buys nothing and costs the native overwrite prompt. So update classifies every
418
- // accepted member first and omits the digest-equal ones from the native add skipping is the ONLY
419
- // way an unchanged member avoids the prompt, because the native CLI is the sole write authority and
420
- // the coordinator may not copy, link, or overwrite anything itself.
458
+ // would install buys nothing and replaces identical bytes. So update classifies every accepted
459
+ // member first and omits the digest-equal ones from the native add. The native CLI remains the sole
460
+ // write authority; the coordinator may not copy, link, or overwrite skill trees itself.
421
461
  //
422
462
  // The digest semantics are borrowed, never re-invented: first-party members digest their PUBLISHED
423
463
  // content (`published: true`) on both transports, external members digest the exact reviewed tree,
@@ -445,14 +485,25 @@ export function expectedMemberDigest(source, member, { localRoot }) {
445
485
 
446
486
  // Classify one installed member directory against its expectation. `skillsRoot` is the canonical
447
487
  // installed store (`~/.agents/skills`); the member lives under it by installName.
448
- export function classifyInstalledMember(source, member, { localRoot, skillsRoot }) {
488
+ export function classifyInstalledMember(source, member, { enableTdd = true, localRoot, skillsRoot }) {
449
489
  const directory = join(skillsRoot, member.installName);
450
490
  const entry = { member: member.installName, sourceId: source?.id ?? null, directory };
451
491
  if (!statSync(directory, { throwIfNoEntry: false })?.isDirectory()) {
452
492
  return { ...entry, status: 'missing' };
453
493
  }
454
494
  const expectedDigest = expectedMemberDigest(source, member, { localRoot });
455
- const actualDigest = contentDigest(directory, { published: firstPartySource(source) });
495
+ let actualDigest;
496
+ try {
497
+ const policyInjections = member.installName === 'tdd' && member.invocationPolicy && !enableTdd
498
+ ? reviewedSourceInjections(directory, member.invocationPolicy)
499
+ : [];
500
+ actualDigest = contentDigest(directory, {
501
+ injected: policyInjections,
502
+ published: firstPartySource(source),
503
+ });
504
+ } catch {
505
+ return { ...entry, status: 'refresh', expectedDigest, actualDigest: null };
506
+ }
456
507
  return {
457
508
  ...entry,
458
509
  status: classifyMember({ installed: true, expectedDigest, actualDigest }),
@@ -465,7 +516,7 @@ export function classifyInstalledMember(source, member, { localRoot, skillsRoot
465
516
  // deduplicated across groups (the shared support runtime classifies once). The buckets drive both
466
517
  // the planner's skip set and the dry-run/no-op rendering. A member the recipe names but its source
467
518
  // does not declare gets no expected digest — refresh-or-missing, never a silent skip.
468
- export function classifySelectedMembers(action, groupIds, { skillsRoot, ...transport } = {}) {
519
+ export function classifySelectedMembers(action, groupIds, { enableTdd = true, skillsRoot, ...transport } = {}) {
469
520
  if (!ACTIONS.has(action)) throw new Error(`Unsupported bundle action: ${action}`);
470
521
  const { recipesPath, localRoot } = resolveBundleTransport(transport);
471
522
  const recipes = readRecipes(recipesPath);
@@ -484,7 +535,7 @@ export function classifySelectedMembers(action, groupIds, { skillsRoot, ...trans
484
535
  if (seen.has(installName)) continue;
485
536
  seen.add(installName);
486
537
  const member = membersByName.get(installName) ?? { installName };
487
- const entry = classifyInstalledMember(source, member, { localRoot, skillsRoot });
538
+ const entry = classifyInstalledMember(source, member, { enableTdd, localRoot, skillsRoot });
488
539
  buckets[entry.status].push(entry);
489
540
  }
490
541
  }
@@ -510,19 +561,21 @@ function printUpdateBuckets(stdout, buckets) {
510
561
  function usage() {
511
562
  return `Usage:
512
563
  node skills/distribution/scripts/bundles.mjs list [--recipes <path>]
513
- node skills/distribution/scripts/bundles.mjs <install|update> [--group <id> ... | --all] [--dry-run] [--yes] [--recipes <path>] [--local-root <path>]
564
+ node skills/distribution/scripts/bundles.mjs <install|update> [--group <id> ... | --all] [--enable-tdd|--disable-tdd] [--dry-run] [--yes] [--recipes <path>] [--local-root <path>]
514
565
 
515
566
  install without --group/--all opens the interactive picker; update without them replays the
516
567
  remembered selection (\${XDG_STATE_HOME:-~/.local/state}/1aboveio-skills/selection.json).
517
568
  When a remembered group's current recipe declares members you have not accepted yet, each new skill
518
569
  is offered individually and defaults to No; --yes accepts them all, --dry-run lists them without
519
- prompting.
570
+ prompting. TDD implicit model invocation is enabled by default; --disable-tdd makes TDD explicit-only,
571
+ and --enable-tdd restores implicit invocation. --manual-tdd remains a compatibility alias for
572
+ --disable-tdd.
520
573
  `;
521
574
  }
522
575
 
523
576
  function parseArgs(args) {
524
577
  const [action = 'install', ...options] = args;
525
- const parsed = { action, groupIds: [], all: false, dryRun: false, yes: false };
578
+ const parsed = { action, groupIds: [], all: false, dryRun: false, yes: false, tddMode: null };
526
579
  for (let index = 0; index < options.length; index += 1) {
527
580
  const option = options[index];
528
581
  if (option === '--group') {
@@ -538,9 +591,15 @@ function parseArgs(args) {
538
591
  } else if (option === '--all') parsed.all = true;
539
592
  else if (option === '--dry-run') parsed.dryRun = true;
540
593
  else if (option === '--yes') parsed.yes = true;
594
+ else if (option === '--enable-tdd') parsed.tddMode = 'implicit';
595
+ else if (option === '--disable-tdd' || option === '--manual-tdd') parsed.tddMode = 'manual-only';
541
596
  else throw new Error(`Unknown option: ${option}`);
542
597
  }
543
598
  if (parsed.all && parsed.groupIds.length > 0) throw new Error('Use either --all or --group, not both');
599
+ const tddOptions = options.filter((option) => ['--enable-tdd', '--disable-tdd', '--manual-tdd'].includes(option));
600
+ if (tddOptions.length > 1) {
601
+ throw new Error('Use only one of --enable-tdd or --disable-tdd');
602
+ }
544
603
  return parsed;
545
604
  }
546
605
 
@@ -683,6 +742,11 @@ async function consentNewMembers(candidates, { yes, stdin, stdout, stderr }) {
683
742
  return declined;
684
743
  }
685
744
 
745
+ function selectionIncludesTdd(groupIds, catalog) {
746
+ const selected = new Set(groupIds);
747
+ return catalog.some((bundle) => selected.has(bundle.id) && bundle.members.includes('tdd'));
748
+ }
749
+
686
750
  // Dependencies are injectable so the coordinator seam can be tested black-box: a stubbed native
687
751
  // executor, controllable TTY streams, and a fixture XDG environment, without reaching process
688
752
  // globals. The default execute is the real shell out to the native Skills CLI — the sole
@@ -690,6 +754,7 @@ async function consentNewMembers(candidates, { yes, stdin, stdout, stderr }) {
690
754
  // AFTER those native commands settle.
691
755
  export async function runCli(args, {
692
756
  execute,
757
+ applyPolicy,
693
758
  stdin = process.stdin,
694
759
  stdout = process.stdout,
695
760
  stderr = process.stderr,
@@ -726,6 +791,17 @@ export async function runCli(args, {
726
791
  priorState = loadPriorSelection(env);
727
792
  }
728
793
  if (groupIds.length === 0) throw new Error('Select at least one bundle');
794
+ const includesTdd = selectionIncludesTdd(groupIds, catalog);
795
+ if (parsed.tddMode && !includesTdd) {
796
+ throw new Error(`${parsed.tddMode === 'implicit' ? '--enable-tdd' : '--disable-tdd'} requires a selected bundle containing tdd`);
797
+ }
798
+ let enableTdd = typeof priorState?.tddImplicitInvocation === 'boolean'
799
+ ? priorState.tddImplicitInvocation
800
+ : true;
801
+ if (includesTdd) {
802
+ if (parsed.tddMode) enableTdd = parsed.tddMode === 'implicit';
803
+ stdout.write(`TDD invocation: ${enableTdd ? 'enabled' : 'disabled'}\n`);
804
+ }
729
805
  // New-member consent (#1128): newly declared user-facing members of a group with prior
730
806
  // acceptance are offered one at a time, defaulting to No. A dry run only LISTS the would-be
731
807
  // candidates — it never prompts and never writes state.
@@ -740,13 +816,13 @@ export async function runCli(args, {
740
816
  }
741
817
  // Digest skip (#1127): `update` classifies the accepted members against the installed tree and
742
818
  // omits the digest-equal ones from the native add. `install` never skips — it is an explicit
743
- // request to install, whose overwrite consent belongs to the native prompt. Declined new
744
- // members (#1128) are excluded from classification: they never run, so they are not buckets.
819
+ // request to install/replace the selected members. Declined new members (#1128) are excluded
820
+ // from classification: they never run, so they are not buckets.
745
821
  let buckets;
746
822
  let skipMembers;
823
+ const skillsRoot = join(env.HOME || homedir(), '.agents', 'skills');
747
824
  if (parsed.action === 'update') {
748
- const skillsRoot = join(env.HOME || homedir(), '.agents', 'skills');
749
- buckets = classifySelectedMembers(parsed.action, groupIds, { ...transport, skillsRoot });
825
+ buckets = classifySelectedMembers(parsed.action, groupIds, { ...transport, enableTdd, skillsRoot });
750
826
  skipMembers = new Set(buckets.unchanged.map((entry) => entry.member));
751
827
  if (declined.size > 0) {
752
828
  for (const key of Object.keys(buckets)) {
@@ -754,7 +830,12 @@ export async function runCli(args, {
754
830
  }
755
831
  }
756
832
  }
757
- const commands = planBundleCommands(parsed.action, groupIds, { ...transport, yes: parsed.yes, skipMembers, omitMembers: declined });
833
+ const commands = planBundleCommands(parsed.action, groupIds, {
834
+ ...transport,
835
+ enableTdd,
836
+ skipMembers,
837
+ omitMembers: declined,
838
+ });
758
839
  if (parsed.dryRun) {
759
840
  if (buckets) printUpdateBuckets(stdout, buckets);
760
841
  for (const planned of commands) {
@@ -769,7 +850,13 @@ export async function runCli(args, {
769
850
  const skipping = buckets.unchanged.map((entry) => entry.member).filter((member) => !SUPPORT_GROUPS.has(member));
770
851
  if (skipping.length > 0) stdout.write(`Up to date, skipping: ${skipping.join(', ')}\n`);
771
852
  }
772
- const { succeeded, failed } = runBundleCommands(commands, { execute, stdout, stderr });
853
+ const { succeeded, failed } = runBundleCommands(commands, {
854
+ execute,
855
+ applyPolicy,
856
+ skillsRoot,
857
+ stdout,
858
+ stderr,
859
+ });
773
860
  printRunSummary(stdout, {
774
861
  succeeded,
775
862
  failed,
@@ -791,6 +878,7 @@ export async function runCli(args, {
791
878
  renderTarget,
792
879
  succeededMembers,
793
880
  omitMembers: declined,
881
+ tddImplicitInvocation: enableTdd,
794
882
  });
795
883
  if (selection.groups.length > 0) {
796
884
  writeSelectionState(selection, env);