@patwoodman/dsh-sdlc-flow 0.4.0-beta.19 → 0.4.0-beta.21

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/cordis.patch.yml CHANGED
@@ -22,10 +22,11 @@
22
22
  #
23
23
  # Order matters only where a row waits on a service: services first, then the
24
24
  # tools and preset rows that use them, then the UI. The
25
- # `agent-preset-registry` row stays FIRST inside the presets block (the dialect
26
- # has no insert-if-missing), so on the web template the composed lead holds two
27
- # same-id registry rows at one service — the loader accepts both and every
28
- # preset activates (verified live); M0-02 pins those counts.
25
+ # `agent-preset-registry` row stays FIRST inside the presets member block
26
+ # (headless/sdk, the dialect has no insert-if-missing). This generated lead
27
+ # patch replaces that insert with an id-targeted override of the web template's
28
+ # shipped row: a duplicate id+name is loader-accepted but blocks plugin-manager
29
+ # uninstall. M0-02 pins one registry row whose default is sdlc-lead.
29
30
 
30
31
  # ── the meta-bundle's own row ────────────────────────────────────────────────
31
32
  # A row that executes this package's apply() at boot, so the meta-bundle is a
@@ -82,11 +83,16 @@
82
83
  worktreePath: !!js process.cwd()
83
84
 
84
85
  # ── from @patwoodman/dsh-sdlc-flow-presets — the 26 role presets (C.3)
86
+ # The web template ships agent-preset-registry. Override its default instead
87
+ # of inserting a duplicate row: a duplicate id+name is loader-accepted but
88
+ # blocks plugin-manager uninstall as "bundle-in-use". The presets member keeps
89
+ # the insert for headless/sdk templates, which have no registry row.
90
+ - id: agent-preset-registry
91
+ name: '@deepseek-ai/dsh-agent-preset-registry'
92
+ config:
93
+ default: sdlc-lead
94
+
85
95
  - insert:
86
- - id: agent-preset-registry
87
- name: "@deepseek-ai/dsh-agent-preset-registry"
88
- config:
89
- default: sdlc-lead
90
96
  - id: preset-sdlc-lead
91
97
  name: "@deepseek-ai/dsh-agent-preset"
92
98
  config:
@@ -106,14 +112,20 @@
106
112
  name: "@deepseek-ai/dsh-tool-fs"
107
113
  - id: tool-todo
108
114
  name: "@deepseek-ai/dsh-tool-todo"
115
+ config:
116
+ allowParallelInProgress: true
109
117
  - id: sdlc-report
110
- name: "@patwoodman/dsh-sdlc-flow/tools"
118
+ name: "@patwoodman/dsh-sdlc-flow/tools-report"
111
119
  - id: tool-subagent
112
120
  name: "@deepseek-ai/dsh-tool-subagent"
121
+ config:
122
+ provider: spawn
123
+ toolName: subagent
124
+ backgroundMode: continuable
113
125
  - id: sdlc-dispatch
114
- name: "@patwoodman/dsh-sdlc-flow/tools"
126
+ name: "@patwoodman/dsh-sdlc-flow/tools-dispatch"
115
127
  - id: sdlc-plan
116
- name: "@patwoodman/dsh-sdlc-flow/tools"
128
+ name: "@patwoodman/dsh-sdlc-flow/tools-plan"
117
129
  - id: preset-sdlc-business-analyst
118
130
  name: "@deepseek-ai/dsh-agent-preset"
119
131
  config:
@@ -133,8 +145,10 @@
133
145
  name: "@deepseek-ai/dsh-tool-fs"
134
146
  - id: tool-todo
135
147
  name: "@deepseek-ai/dsh-tool-todo"
148
+ config:
149
+ allowParallelInProgress: false
136
150
  - id: sdlc-report
137
- name: "@patwoodman/dsh-sdlc-flow/tools"
151
+ name: "@patwoodman/dsh-sdlc-flow/tools-report"
138
152
  - id: preset-sdlc-requirements-engineer
139
153
  name: "@deepseek-ai/dsh-agent-preset"
140
154
  config:
@@ -154,8 +168,10 @@
154
168
  name: "@deepseek-ai/dsh-tool-fs"
155
169
  - id: tool-todo
156
170
  name: "@deepseek-ai/dsh-tool-todo"
171
+ config:
172
+ allowParallelInProgress: false
157
173
  - id: sdlc-report
158
- name: "@patwoodman/dsh-sdlc-flow/tools"
174
+ name: "@patwoodman/dsh-sdlc-flow/tools-report"
159
175
  - id: preset-sdlc-product-owner
160
176
  name: "@deepseek-ai/dsh-agent-preset"
161
177
  config:
@@ -175,8 +191,10 @@
175
191
  name: "@deepseek-ai/dsh-tool-fs"
176
192
  - id: tool-todo
177
193
  name: "@deepseek-ai/dsh-tool-todo"
194
+ config:
195
+ allowParallelInProgress: false
178
196
  - id: sdlc-report
179
- name: "@patwoodman/dsh-sdlc-flow/tools"
197
+ name: "@patwoodman/dsh-sdlc-flow/tools-report"
180
198
  - id: preset-sdlc-solution-architect
181
199
  name: "@deepseek-ai/dsh-agent-preset"
182
200
  config:
@@ -196,8 +214,10 @@
196
214
  name: "@deepseek-ai/dsh-tool-fs"
197
215
  - id: tool-todo
198
216
  name: "@deepseek-ai/dsh-tool-todo"
217
+ config:
218
+ allowParallelInProgress: false
199
219
  - id: sdlc-report
200
- name: "@patwoodman/dsh-sdlc-flow/tools"
220
+ name: "@patwoodman/dsh-sdlc-flow/tools-report"
201
221
  - id: preset-sdlc-security-engineer
202
222
  name: "@deepseek-ai/dsh-agent-preset"
203
223
  config:
@@ -217,8 +237,10 @@
217
237
  name: "@deepseek-ai/dsh-tool-fs"
218
238
  - id: tool-todo
219
239
  name: "@deepseek-ai/dsh-tool-todo"
240
+ config:
241
+ allowParallelInProgress: false
220
242
  - id: sdlc-report
221
- name: "@patwoodman/dsh-sdlc-flow/tools"
243
+ name: "@patwoodman/dsh-sdlc-flow/tools-report"
222
244
  - id: preset-sdlc-data-engineer
223
245
  name: "@deepseek-ai/dsh-agent-preset"
224
246
  config:
@@ -238,8 +260,10 @@
238
260
  name: "@deepseek-ai/dsh-tool-fs"
239
261
  - id: tool-todo
240
262
  name: "@deepseek-ai/dsh-tool-todo"
263
+ config:
264
+ allowParallelInProgress: false
241
265
  - id: sdlc-report
242
- name: "@patwoodman/dsh-sdlc-flow/tools"
266
+ name: "@patwoodman/dsh-sdlc-flow/tools-report"
243
267
  - id: preset-sdlc-database-engineer
244
268
  name: "@deepseek-ai/dsh-agent-preset"
245
269
  config:
@@ -259,8 +283,10 @@
259
283
  name: "@deepseek-ai/dsh-tool-fs"
260
284
  - id: tool-todo
261
285
  name: "@deepseek-ai/dsh-tool-todo"
286
+ config:
287
+ allowParallelInProgress: false
262
288
  - id: sdlc-report
263
- name: "@patwoodman/dsh-sdlc-flow/tools"
289
+ name: "@patwoodman/dsh-sdlc-flow/tools-report"
264
290
  - id: preset-sdlc-ux-designer
265
291
  name: "@deepseek-ai/dsh-agent-preset"
266
292
  config:
@@ -280,8 +306,10 @@
280
306
  name: "@deepseek-ai/dsh-tool-fs"
281
307
  - id: tool-todo
282
308
  name: "@deepseek-ai/dsh-tool-todo"
309
+ config:
310
+ allowParallelInProgress: false
283
311
  - id: sdlc-report
284
- name: "@patwoodman/dsh-sdlc-flow/tools"
312
+ name: "@patwoodman/dsh-sdlc-flow/tools-report"
285
313
  - id: preset-sdlc-ui-designer
286
314
  name: "@deepseek-ai/dsh-agent-preset"
287
315
  config:
@@ -301,8 +329,10 @@
301
329
  name: "@deepseek-ai/dsh-tool-fs"
302
330
  - id: tool-todo
303
331
  name: "@deepseek-ai/dsh-tool-todo"
332
+ config:
333
+ allowParallelInProgress: false
304
334
  - id: sdlc-report
305
- name: "@patwoodman/dsh-sdlc-flow/tools"
335
+ name: "@patwoodman/dsh-sdlc-flow/tools-report"
306
336
  - id: preset-sdlc-accessibility-engineer
307
337
  name: "@deepseek-ai/dsh-agent-preset"
308
338
  config:
@@ -322,8 +352,10 @@
322
352
  name: "@deepseek-ai/dsh-tool-fs"
323
353
  - id: tool-todo
324
354
  name: "@deepseek-ai/dsh-tool-todo"
355
+ config:
356
+ allowParallelInProgress: false
325
357
  - id: sdlc-report
326
- name: "@patwoodman/dsh-sdlc-flow/tools"
358
+ name: "@patwoodman/dsh-sdlc-flow/tools-report"
327
359
  - id: preset-sdlc-frontend-engineer
328
360
  name: "@deepseek-ai/dsh-agent-preset"
329
361
  config:
@@ -343,8 +375,10 @@
343
375
  name: "@deepseek-ai/dsh-tool-fs"
344
376
  - id: tool-todo
345
377
  name: "@deepseek-ai/dsh-tool-todo"
378
+ config:
379
+ allowParallelInProgress: false
346
380
  - id: sdlc-report
347
- name: "@patwoodman/dsh-sdlc-flow/tools"
381
+ name: "@patwoodman/dsh-sdlc-flow/tools-report"
348
382
  - id: preset-sdlc-backend-engineer
349
383
  name: "@deepseek-ai/dsh-agent-preset"
350
384
  config:
@@ -364,8 +398,10 @@
364
398
  name: "@deepseek-ai/dsh-tool-fs"
365
399
  - id: tool-todo
366
400
  name: "@deepseek-ai/dsh-tool-todo"
401
+ config:
402
+ allowParallelInProgress: false
367
403
  - id: sdlc-report
368
- name: "@patwoodman/dsh-sdlc-flow/tools"
404
+ name: "@patwoodman/dsh-sdlc-flow/tools-report"
369
405
  - id: preset-sdlc-api-engineer
370
406
  name: "@deepseek-ai/dsh-agent-preset"
371
407
  config:
@@ -385,8 +421,10 @@
385
421
  name: "@deepseek-ai/dsh-tool-fs"
386
422
  - id: tool-todo
387
423
  name: "@deepseek-ai/dsh-tool-todo"
424
+ config:
425
+ allowParallelInProgress: false
388
426
  - id: sdlc-report
389
- name: "@patwoodman/dsh-sdlc-flow/tools"
427
+ name: "@patwoodman/dsh-sdlc-flow/tools-report"
390
428
  - id: preset-sdlc-integration-engineer
391
429
  name: "@deepseek-ai/dsh-agent-preset"
392
430
  config:
@@ -406,8 +444,10 @@
406
444
  name: "@deepseek-ai/dsh-tool-fs"
407
445
  - id: tool-todo
408
446
  name: "@deepseek-ai/dsh-tool-todo"
447
+ config:
448
+ allowParallelInProgress: false
409
449
  - id: sdlc-report
410
- name: "@patwoodman/dsh-sdlc-flow/tools"
450
+ name: "@patwoodman/dsh-sdlc-flow/tools-report"
411
451
  - id: preset-sdlc-devops-engineer
412
452
  name: "@deepseek-ai/dsh-agent-preset"
413
453
  config:
@@ -427,8 +467,10 @@
427
467
  name: "@deepseek-ai/dsh-tool-fs"
428
468
  - id: tool-todo
429
469
  name: "@deepseek-ai/dsh-tool-todo"
470
+ config:
471
+ allowParallelInProgress: false
430
472
  - id: sdlc-report
431
- name: "@patwoodman/dsh-sdlc-flow/tools"
473
+ name: "@patwoodman/dsh-sdlc-flow/tools-report"
432
474
  - id: preset-sdlc-platform-engineer
433
475
  name: "@deepseek-ai/dsh-agent-preset"
434
476
  config:
@@ -448,8 +490,10 @@
448
490
  name: "@deepseek-ai/dsh-tool-fs"
449
491
  - id: tool-todo
450
492
  name: "@deepseek-ai/dsh-tool-todo"
493
+ config:
494
+ allowParallelInProgress: false
451
495
  - id: sdlc-report
452
- name: "@patwoodman/dsh-sdlc-flow/tools"
496
+ name: "@patwoodman/dsh-sdlc-flow/tools-report"
453
497
  - id: preset-sdlc-qa-engineer
454
498
  name: "@deepseek-ai/dsh-agent-preset"
455
499
  config:
@@ -469,8 +513,10 @@
469
513
  name: "@deepseek-ai/dsh-tool-fs"
470
514
  - id: tool-todo
471
515
  name: "@deepseek-ai/dsh-tool-todo"
516
+ config:
517
+ allowParallelInProgress: false
472
518
  - id: sdlc-report
473
- name: "@patwoodman/dsh-sdlc-flow/tools"
519
+ name: "@patwoodman/dsh-sdlc-flow/tools-report"
474
520
  - id: preset-sdlc-test-automation-engineer
475
521
  name: "@deepseek-ai/dsh-agent-preset"
476
522
  config:
@@ -490,8 +536,10 @@
490
536
  name: "@deepseek-ai/dsh-tool-fs"
491
537
  - id: tool-todo
492
538
  name: "@deepseek-ai/dsh-tool-todo"
539
+ config:
540
+ allowParallelInProgress: false
493
541
  - id: sdlc-report
494
- name: "@patwoodman/dsh-sdlc-flow/tools"
542
+ name: "@patwoodman/dsh-sdlc-flow/tools-report"
495
543
  - id: preset-sdlc-e2e-engineer
496
544
  name: "@deepseek-ai/dsh-agent-preset"
497
545
  config:
@@ -511,8 +559,10 @@
511
559
  name: "@deepseek-ai/dsh-tool-fs"
512
560
  - id: tool-todo
513
561
  name: "@deepseek-ai/dsh-tool-todo"
562
+ config:
563
+ allowParallelInProgress: false
514
564
  - id: sdlc-report
515
- name: "@patwoodman/dsh-sdlc-flow/tools"
565
+ name: "@patwoodman/dsh-sdlc-flow/tools-report"
516
566
  - id: preset-sdlc-visual-qa-engineer
517
567
  name: "@deepseek-ai/dsh-agent-preset"
518
568
  config:
@@ -532,8 +582,10 @@
532
582
  name: "@deepseek-ai/dsh-tool-fs"
533
583
  - id: tool-todo
534
584
  name: "@deepseek-ai/dsh-tool-todo"
585
+ config:
586
+ allowParallelInProgress: false
535
587
  - id: sdlc-report
536
- name: "@patwoodman/dsh-sdlc-flow/tools"
588
+ name: "@patwoodman/dsh-sdlc-flow/tools-report"
537
589
  - id: preset-sdlc-performance-engineer
538
590
  name: "@deepseek-ai/dsh-agent-preset"
539
591
  config:
@@ -553,8 +605,10 @@
553
605
  name: "@deepseek-ai/dsh-tool-fs"
554
606
  - id: tool-todo
555
607
  name: "@deepseek-ai/dsh-tool-todo"
608
+ config:
609
+ allowParallelInProgress: false
556
610
  - id: sdlc-report
557
- name: "@patwoodman/dsh-sdlc-flow/tools"
611
+ name: "@patwoodman/dsh-sdlc-flow/tools-report"
558
612
  - id: preset-sdlc-technical-writer
559
613
  name: "@deepseek-ai/dsh-agent-preset"
560
614
  config:
@@ -574,8 +628,10 @@
574
628
  name: "@deepseek-ai/dsh-tool-fs"
575
629
  - id: tool-todo
576
630
  name: "@deepseek-ai/dsh-tool-todo"
631
+ config:
632
+ allowParallelInProgress: false
577
633
  - id: sdlc-report
578
- name: "@patwoodman/dsh-sdlc-flow/tools"
634
+ name: "@patwoodman/dsh-sdlc-flow/tools-report"
579
635
  - id: preset-sdlc-release-manager
580
636
  name: "@deepseek-ai/dsh-agent-preset"
581
637
  config:
@@ -595,8 +651,10 @@
595
651
  name: "@deepseek-ai/dsh-tool-fs"
596
652
  - id: tool-todo
597
653
  name: "@deepseek-ai/dsh-tool-todo"
654
+ config:
655
+ allowParallelInProgress: false
598
656
  - id: sdlc-report
599
- name: "@patwoodman/dsh-sdlc-flow/tools"
657
+ name: "@patwoodman/dsh-sdlc-flow/tools-report"
600
658
  - id: preset-sdlc-code-reviewer
601
659
  name: "@deepseek-ai/dsh-agent-preset"
602
660
  config:
@@ -616,8 +674,10 @@
616
674
  name: "@deepseek-ai/dsh-tool-fs"
617
675
  - id: tool-todo
618
676
  name: "@deepseek-ai/dsh-tool-todo"
677
+ config:
678
+ allowParallelInProgress: false
619
679
  - id: sdlc-report
620
- name: "@patwoodman/dsh-sdlc-flow/tools"
680
+ name: "@patwoodman/dsh-sdlc-flow/tools-report"
621
681
  - id: preset-sdlc-sre-engineer
622
682
  name: "@deepseek-ai/dsh-agent-preset"
623
683
  config:
@@ -637,8 +697,10 @@
637
697
  name: "@deepseek-ai/dsh-tool-fs"
638
698
  - id: tool-todo
639
699
  name: "@deepseek-ai/dsh-tool-todo"
700
+ config:
701
+ allowParallelInProgress: false
640
702
  - id: sdlc-report
641
- name: "@patwoodman/dsh-sdlc-flow/tools"
703
+ name: "@patwoodman/dsh-sdlc-flow/tools-report"
642
704
  - id: sdlc-presets
643
705
  name: "@patwoodman/dsh-sdlc-flow/presets-plugin"
644
706
  config: {}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * packages/sdlc-orchestrator/src/install-lib.mjs — pure text helpers for the
3
+ * registry installer. Kept separate from `install.mjs` so tests can import it
4
+ * without running the CLI.
5
+ */
6
+
7
+ /**
8
+ * Normalize `minimumReleaseAgeExclude` in a pnpm-workspace.yaml body and add
9
+ * exact-version entries for the suite.
10
+ *
11
+ * pnpm accepts ONE `name@version` pattern per list item. A value such as
12
+ * `@patwoodman/dsh-sdlc-flow@0.4.0-beta.19 || 0.4.0-beta.20` (written by an
13
+ * earlier install path) is not a valid pattern, so pnpm ignores the exemption
14
+ * and a fresh beta silently resolves to the previous one. This splits such
15
+ * entries into valid items, carries the package name onto a bare version, adds
16
+ * the requested specs, deduplicates, sorts, and leaves every other setting in
17
+ * the file untouched.
18
+ *
19
+ * @param {string} text - existing pnpm-workspace.yaml (may be empty).
20
+ * @param {string[]} specs - exact `name@version` patterns to add.
21
+ * @returns {string} the rewritten YAML body with a trailing newline.
22
+ */
23
+ export function normalizeReleaseAgeExcludes(text, specs) {
24
+ const lines = String(text ?? '').split('\n');
25
+ const start = lines.findIndex((line) => /^minimumReleaseAgeExclude:\s*$/.test(line));
26
+ const wanted = [];
27
+ if (start >= 0) {
28
+ let end = start + 1;
29
+ while (end < lines.length && (/^\s*-\s+/.test(lines[end]) || lines[end].trim() === '')) end += 1;
30
+ for (const line of lines.slice(start + 1, end)) {
31
+ const raw = line.replace(/^\s*-\s+/, '').trim();
32
+ if (!raw) continue;
33
+ const value = raw.replace(/^['"]|['"]$/g, '');
34
+ let packageName = null;
35
+ for (const part of value.split(/\s*\|\|\s*/)) {
36
+ const trimmed = part.trim();
37
+ if (!trimmed) continue;
38
+ const named = /^((?:@[^/]+\/)?[^@]+)@(.+)$/.exec(trimmed);
39
+ if (named) {
40
+ packageName = named[1];
41
+ wanted.push(trimmed);
42
+ } else if (packageName && /^[0-9]/.test(trimmed)) {
43
+ wanted.push(`${packageName}@${trimmed}`);
44
+ } else {
45
+ wanted.push(trimmed);
46
+ }
47
+ }
48
+ }
49
+ lines.splice(start, end - start);
50
+ }
51
+ wanted.push(...specs);
52
+ const unique = [...new Set(wanted.filter(Boolean))].sort();
53
+ lines.push('minimumReleaseAgeExclude:');
54
+ for (const spec of unique) lines.push(` - '${spec}'`);
55
+ return `${lines.join('\n').replace(/\n+$/, '')}\n`;
56
+ }
@@ -0,0 +1,274 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * packages/sdlc-orchestrator/src/install.mjs — registry installer/updater.
4
+ *
5
+ * The Desktop Add-plugin field and `dsh plugin add` both go through pnpm, and
6
+ * pnpm 11 applies a 24-hour `minimumReleaseAge` to `latest`/range requests. A
7
+ * beta published minutes ago therefore resolves to the PREVIOUS beta — exactly
8
+ * how beta.19 (broken presets) kept installing while beta.20 was already
9
+ * `latest`. This command is the reliable path:
10
+ *
11
+ * 1. resolve `latest` (or `beta`) from the npm registry itself;
12
+ * 2. pin the EXACT version in the profile and add the matching
13
+ * `minimumReleaseAgeExclude` entry (pnpm's documented escape hatch);
14
+ * 3. create the profile from its shipped template when it is missing;
15
+ * 4. install with `dsh plugin add <name>@<exact>`;
16
+ * 5. re-add the bundle to `dsh.profile.bundles` (a failed uninstall can
17
+ * leave a package installed but disabled) and verify the installed
18
+ * package.json version.
19
+ *
20
+ * Run it without cloning:
21
+ *
22
+ * npx -y @patwoodman/dsh-sdlc-flow@latest dsh-sdlc-install
23
+ * npx -y @patwoodman/dsh-sdlc-flow@latest dsh-sdlc-install --only all
24
+ *
25
+ * Flags: --only lead|worker|probe|all (default lead), --tag latest|beta,
26
+ * --version <exact>, --dsh-home <dir>, --dry-run, --json, --help.
27
+ *
28
+ * Node built-ins only: this file is shipped in the published package and must
29
+ * run before any dependency is installed.
30
+ */
31
+
32
+ import { spawnSync } from 'node:child_process';
33
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
34
+ import { homedir } from 'node:os';
35
+ import { join } from 'node:path';
36
+
37
+ import { PACKAGE, BUNDLES } from './index.js';
38
+ import { normalizeReleaseAgeExcludes } from './install-lib.mjs';
39
+
40
+ const REGISTRY = process.env.npm_config_registry || 'https://registry.npmjs.org';
41
+ const META = PACKAGE;
42
+ const CORE = BUNDLES.find((name) => name.endsWith('-core'));
43
+ const TOOLS = BUNDLES.find((name) => name.endsWith('-tools'));
44
+ const PRESETS = BUNDLES.find((name) => name.endsWith('-presets'));
45
+ const UI = BUNDLES.find((name) => name.endsWith('-ui'));
46
+ const GUARD = BUNDLES.find((name) => name.endsWith('-guard')) ?? `${META}-guard`;
47
+
48
+ const argv = process.argv.slice(2);
49
+ const has = (name) => argv.includes(name);
50
+ const flag = (name, fallback) => (has(name) ? argv[argv.indexOf(name) + 1] : fallback);
51
+
52
+ const ONLY = flag('--only', 'lead');
53
+ const TAG = flag('--tag', 'latest');
54
+ const EXACT = flag('--version', null);
55
+ const DSH_HOME = flag('--dsh-home', process.env.DSH_HOME || join(homedir(), '.dsh'));
56
+ const DRY_RUN = has('--dry-run');
57
+ const JSON_OUT = has('--json');
58
+
59
+ if (has('--help') || has('-h')) {
60
+ process.stdout.write(`Usage: dsh-sdlc-install [options]
61
+
62
+ Install the latest published DSH SDLC Flow bundle into a DSH profile.
63
+
64
+ --only lead|worker|probe|all which profile(s) to install (default: lead)
65
+ --tag latest|beta registry dist-tag to resolve (default: latest)
66
+ --version <exact> pin an exact version instead of a tag
67
+ --dsh-home <dir> DSH home (default: $DSH_HOME or ~/.dsh)
68
+ --dry-run print the commands without changing anything
69
+ --json machine-readable result
70
+ -h, --help this help
71
+
72
+ Examples:
73
+ npx -y @patwoodman/dsh-sdlc-flow@latest dsh-sdlc-install
74
+ npx -y @patwoodman/dsh-sdlc-flow@latest dsh-sdlc-install --only all
75
+ npx -y @patwoodman/dsh-sdlc-flow@latest dsh-sdlc-install --version 0.4.0-beta.21
76
+ `);
77
+ process.exit(0);
78
+ }
79
+
80
+ const VALID_ONLY = new Set(['lead', 'worker', 'probe', 'all']);
81
+ if (!VALID_ONLY.has(ONLY)) {
82
+ process.stderr.write(`dsh-sdlc-install: --only must be one of ${[...VALID_ONLY].join(', ')}\n`);
83
+ process.exit(2);
84
+ }
85
+ if (!['latest', 'beta'].includes(TAG) && EXACT === null) {
86
+ process.stderr.write('dsh-sdlc-install: --tag must be latest or beta\n');
87
+ process.exit(2);
88
+ }
89
+
90
+ const steps = [];
91
+ function record(step, detail = '') {
92
+ steps.push({ step, detail });
93
+ if (!JSON_OUT) process.stdout.write(` [ok] ${step}${detail ? ` — ${detail}` : ''}\n`);
94
+ }
95
+ function warn(message) {
96
+ steps.push({ step: message, warning: true });
97
+ if (!JSON_OUT) process.stdout.write(` [warn] ${message}\n`);
98
+ }
99
+
100
+ function run(command, args, { cwd, timeoutMs = 900_000 } = {}) {
101
+ if (DRY_RUN) {
102
+ record(`exec ${command} ${args.join(' ')}`);
103
+ return { status: 0, stdout: '', stderr: '' };
104
+ }
105
+ const result = spawnSync(command, args, {
106
+ cwd,
107
+ encoding: 'utf8',
108
+ timeout: timeoutMs,
109
+ env: { ...process.env, DSH_HOME },
110
+ });
111
+ return {
112
+ status: result.status ?? 1,
113
+ stdout: result.stdout ?? '',
114
+ stderr: result.stderr ?? '',
115
+ };
116
+ }
117
+
118
+ function mustRun(command, args, context) {
119
+ const result = run(command, args);
120
+ if (result.status !== 0) {
121
+ const detail = (result.stderr || result.stdout || '').trim().slice(-1_500);
122
+ throw new Error(`${context}: \`${command} ${args.join(' ')}\` exited ${result.status}${detail ? `\n${detail}` : ''}`);
123
+ }
124
+ return result;
125
+ }
126
+
127
+ async function resolveVersion() {
128
+ if (EXACT !== null) return { version: EXACT, source: '--version' };
129
+ const url = `${REGISTRY.replace(/\/+$/, '')}/${META.replace('/', '%2F')}`;
130
+ let response;
131
+ try {
132
+ response = await fetch(url, { signal: AbortSignal.timeout(20_000) });
133
+ } catch (error) {
134
+ throw new Error(`cannot reach the npm registry at ${REGISTRY}: ${error.message}\nPin a version with --version <exact> or check your network.`);
135
+ }
136
+ if (!response.ok) throw new Error(`registry returned HTTP ${response.status} for ${META}`);
137
+ const packument = await response.json();
138
+ const version = packument?.['dist-tags']?.[TAG];
139
+ if (typeof version !== 'string') {
140
+ throw new Error(`registry has no ${TAG} dist-tag for ${META} (tags: ${Object.keys(packument?.['dist-tags'] ?? {}).join(', ')})`);
141
+ }
142
+ return { version, source: TAG };
143
+ }
144
+
145
+ const profileDir = (name) => join(DSH_HOME, 'profiles', name);
146
+
147
+ /** Create a profile from its shipped template when absent. */
148
+ function ensureProfile(name, template) {
149
+ const dir = profileDir(name);
150
+ if (existsSync(join(dir, 'package.json'))) {
151
+ record(`profile ${name} already exists`);
152
+ return;
153
+ }
154
+ mustRun('dsh', ['--profile', name, '--from-default-profile', template, '--dump-config'], `create profile ${name}`);
155
+ record(`created profile ${name} from the ${template} template`);
156
+ }
157
+
158
+ /**
159
+ * Add exact-version `minimumReleaseAgeExclude` entries, normalizing the
160
+ * malformed `pkg@a || pkg@b` form (pnpm does not understand it) into separate
161
+ * list items. This is the difference between installing the fresh beta and
162
+ * silently resolving the previous one.
163
+ */
164
+ function ensureReleaseAgeExcludes(name, specs) {
165
+ const file = join(profileDir(name), 'pnpm-workspace.yaml');
166
+ const text = existsSync(file) ? readFileSync(file, 'utf8') : 'packages:\n - .\n';
167
+ const next = normalizeReleaseAgeExcludes(text, specs);
168
+ if (DRY_RUN) {
169
+ record(`would set minimumReleaseAgeExclude (${specs.join(', ')})`);
170
+ return;
171
+ }
172
+ writeFileSync(file, next);
173
+ record('pinned pnpm release-age exemptions', specs.join(', '));
174
+ }
175
+
176
+ /** Ensure the bundle is selected even if an earlier failed uninstall removed it. */
177
+ function ensureBundles(name, bundles) {
178
+ const file = join(profileDir(name), 'package.json');
179
+ if (DRY_RUN) {
180
+ record(`would select bundles ${bundles.join(', ')}`);
181
+ return;
182
+ }
183
+ const manifest = JSON.parse(readFileSync(file, 'utf8'));
184
+ manifest.dsh ??= {};
185
+ manifest.dsh.profile ??= {};
186
+ const current = Array.isArray(manifest.dsh.profile.bundles) ? manifest.dsh.profile.bundles : [];
187
+ const missing = bundles.filter((bundle) => !current.includes(bundle));
188
+ if (missing.length === 0) {
189
+ record(`profile ${name} already selects ${bundles.length} bundle(s)`);
190
+ return;
191
+ }
192
+ manifest.dsh.profile.bundles = [...current, ...missing];
193
+ writeFileSync(file, `${JSON.stringify(manifest, null, 2)}\n`);
194
+ record(`selected bundle(s) in ${name}`, missing.join(', '));
195
+ }
196
+
197
+ function installedVersion(name, pkgName) {
198
+ const manifest = join(profileDir(name), 'node_modules', ...pkgName.split('/'), 'package.json');
199
+ if (!existsSync(manifest)) return null;
200
+ try {
201
+ return JSON.parse(readFileSync(manifest, 'utf8')).version;
202
+ } catch {
203
+ return null;
204
+ }
205
+ }
206
+
207
+ const PLANS = {
208
+ lead: { profile: 'sdlc-web', template: 'web', packages: [META], bundles: [META] },
209
+ worker: { profile: 'sdlc-worker', template: 'headless', packages: [GUARD, TOOLS], bundles: [GUARD, TOOLS] },
210
+ probe: { profile: 'sdlc-probe', template: 'sdk', packages: [CORE, TOOLS, PRESETS, GUARD], bundles: [CORE, TOOLS, PRESETS, GUARD] },
211
+ };
212
+
213
+ function planFor(only) {
214
+ return only === 'all' ? ['lead', 'worker', 'probe'] : [only];
215
+ }
216
+
217
+ async function main() {
218
+ if (!JSON_OUT) {
219
+ process.stdout.write(`\nDSH SDLC Flow — registry install${DRY_RUN ? ' (dry run)' : ''}\n`);
220
+ process.stdout.write('================================================\n\n');
221
+ process.stdout.write(` DSH_HOME : ${DSH_HOME}\n`);
222
+ }
223
+
224
+ const { version, source } = await resolveVersion();
225
+ record(`resolved ${META}@${version}`, `from ${source}`);
226
+
227
+ const dsh = DRY_RUN ? { status: 0, stdout: '' } : run('dsh', ['--version']);
228
+ if (!DRY_RUN && dsh.status !== 0) {
229
+ throw new Error('dsh is not on PATH. Install it first: npm install -g @deepseek-ai/dsh@0.2.1-alpha.2 (or any 0.2.x).');
230
+ }
231
+ record('dsh CLI available', DRY_RUN ? '(dry run)' : dsh.stdout.trim());
232
+
233
+ for (const only of planFor(ONLY)) {
234
+ const plan = PLANS[only];
235
+ if (!plan || plan.packages.some((pkg) => pkg === undefined)) {
236
+ throw new Error(`cannot derive the ${only} package list from ${META}'s BUNDLES export`);
237
+ }
238
+ ensureProfile(plan.profile, plan.template);
239
+ ensureReleaseAgeExcludes(plan.profile, plan.packages.map((pkg) => `${pkg}@${version}`));
240
+ mustRun(
241
+ 'dsh',
242
+ ['plugin', '--profile', plan.profile, 'add', '--ignore-workspace-root-check', ...plan.packages.map((pkg) => `${pkg}@${version}`)],
243
+ `install into ${plan.profile}`,
244
+ );
245
+ record(`installed ${plan.packages.map((pkg) => `${pkg}@${version}`).join(', ')}`, plan.profile);
246
+ ensureBundles(plan.profile, plan.bundles);
247
+ if (!DRY_RUN) {
248
+ for (const pkg of plan.packages) {
249
+ const actual = installedVersion(plan.profile, pkg);
250
+ if (actual !== version) {
251
+ throw new Error(`${plan.profile}: expected ${pkg}@${version}, found ${pkg}@${actual ?? '<missing>'}`);
252
+ }
253
+ }
254
+ record(`verified ${plan.profile}`, plan.packages.map((pkg) => `${pkg}@${version}`).join(', '));
255
+ }
256
+ }
257
+
258
+ if (JSON_OUT) {
259
+ process.stdout.write(`${JSON.stringify({ version, tag: EXACT === null ? TAG : null, only: ONLY, dshHome: DSH_HOME, steps }, null, 2)}\n`);
260
+ } else {
261
+ process.stdout.write('\nDone. Restart DeepSeek Harness (or any running lead) so the new bundle is loaded:\n');
262
+ process.stdout.write(' dsh --profile sdlc-web\n\n');
263
+ if (ONLY === 'lead' || ONLY === 'all') {
264
+ process.stdout.write('A new session will use the sdlc-lead preset. If you still see an "agent-preset/invalid"\n');
265
+ process.stdout.write('error, the app is still running the old bundle — quit it completely and reopen.\n');
266
+ }
267
+ }
268
+ }
269
+
270
+ main().catch((error) => {
271
+ if (JSON_OUT) process.stdout.write(`${JSON.stringify({ ok: false, error: error.message }, null, 2)}\n`);
272
+ else process.stderr.write(`\ndsh-sdlc-install: ${error.message}\n`);
273
+ process.exit(1);
274
+ });
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@patwoodman/dsh-sdlc-flow-cli",
3
- "version": "0.4.0-beta.19",
3
+ "version": "0.4.0-beta.21",
4
4
  "type": "module"
5
5
  }
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@patwoodman/dsh-sdlc-flow-core",
3
- "version": "0.4.0-beta.19",
3
+ "version": "0.4.0-beta.21",
4
4
  "type": "module"
5
5
  }
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@patwoodman/dsh-sdlc-flow-guard",
3
- "version": "0.4.0-beta.19",
3
+ "version": "0.4.0-beta.21",
4
4
  "type": "module"
5
5
  }
@@ -12,10 +12,10 @@
12
12
  #
13
13
  # The agent-preset-registry row is FIRST inside insert: so the registry exists
14
14
  # before the presets register against it on templates that lack one
15
- # (headless). On the web template the shipped row already exists, so the lead
16
- # profile composes two same-id rows pointing at the same service — the loader
17
- # accepts both and the presets activate (verified live). The M0 static check
18
- # pins the patch shape; the M0-02 live check pins the composed profile.
15
+ # (headless/sdk). The web template already ships the row; the generated lead
16
+ # meta-bundle replaces this insert with an id-targeted override — a duplicate
17
+ # id+name is loader-accepted but blocks plugin-manager uninstall. The M0-02
18
+ # live check pins one registry row with default sdlc-lead.
19
19
  - insert:
20
20
  - id: agent-preset-registry
21
21
  name: "@deepseek-ai/dsh-agent-preset-registry"
@@ -40,14 +40,20 @@
40
40
  name: "@deepseek-ai/dsh-tool-fs"
41
41
  - id: tool-todo
42
42
  name: "@deepseek-ai/dsh-tool-todo"
43
+ config:
44
+ allowParallelInProgress: true
43
45
  - id: sdlc-report
44
- name: "@patwoodman/dsh-sdlc-flow-tools"
46
+ name: "@patwoodman/dsh-sdlc-flow-tools/report"
45
47
  - id: tool-subagent
46
48
  name: "@deepseek-ai/dsh-tool-subagent"
49
+ config:
50
+ provider: spawn
51
+ toolName: subagent
52
+ backgroundMode: continuable
47
53
  - id: sdlc-dispatch
48
- name: "@patwoodman/dsh-sdlc-flow-tools"
54
+ name: "@patwoodman/dsh-sdlc-flow-tools/dispatch"
49
55
  - id: sdlc-plan
50
- name: "@patwoodman/dsh-sdlc-flow-tools"
56
+ name: "@patwoodman/dsh-sdlc-flow-tools/plan"
51
57
  - id: preset-sdlc-business-analyst
52
58
  name: "@deepseek-ai/dsh-agent-preset"
53
59
  config:
@@ -67,8 +73,10 @@
67
73
  name: "@deepseek-ai/dsh-tool-fs"
68
74
  - id: tool-todo
69
75
  name: "@deepseek-ai/dsh-tool-todo"
76
+ config:
77
+ allowParallelInProgress: false
70
78
  - id: sdlc-report
71
- name: "@patwoodman/dsh-sdlc-flow-tools"
79
+ name: "@patwoodman/dsh-sdlc-flow-tools/report"
72
80
  - id: preset-sdlc-requirements-engineer
73
81
  name: "@deepseek-ai/dsh-agent-preset"
74
82
  config:
@@ -88,8 +96,10 @@
88
96
  name: "@deepseek-ai/dsh-tool-fs"
89
97
  - id: tool-todo
90
98
  name: "@deepseek-ai/dsh-tool-todo"
99
+ config:
100
+ allowParallelInProgress: false
91
101
  - id: sdlc-report
92
- name: "@patwoodman/dsh-sdlc-flow-tools"
102
+ name: "@patwoodman/dsh-sdlc-flow-tools/report"
93
103
  - id: preset-sdlc-product-owner
94
104
  name: "@deepseek-ai/dsh-agent-preset"
95
105
  config:
@@ -109,8 +119,10 @@
109
119
  name: "@deepseek-ai/dsh-tool-fs"
110
120
  - id: tool-todo
111
121
  name: "@deepseek-ai/dsh-tool-todo"
122
+ config:
123
+ allowParallelInProgress: false
112
124
  - id: sdlc-report
113
- name: "@patwoodman/dsh-sdlc-flow-tools"
125
+ name: "@patwoodman/dsh-sdlc-flow-tools/report"
114
126
  - id: preset-sdlc-solution-architect
115
127
  name: "@deepseek-ai/dsh-agent-preset"
116
128
  config:
@@ -130,8 +142,10 @@
130
142
  name: "@deepseek-ai/dsh-tool-fs"
131
143
  - id: tool-todo
132
144
  name: "@deepseek-ai/dsh-tool-todo"
145
+ config:
146
+ allowParallelInProgress: false
133
147
  - id: sdlc-report
134
- name: "@patwoodman/dsh-sdlc-flow-tools"
148
+ name: "@patwoodman/dsh-sdlc-flow-tools/report"
135
149
  - id: preset-sdlc-security-engineer
136
150
  name: "@deepseek-ai/dsh-agent-preset"
137
151
  config:
@@ -151,8 +165,10 @@
151
165
  name: "@deepseek-ai/dsh-tool-fs"
152
166
  - id: tool-todo
153
167
  name: "@deepseek-ai/dsh-tool-todo"
168
+ config:
169
+ allowParallelInProgress: false
154
170
  - id: sdlc-report
155
- name: "@patwoodman/dsh-sdlc-flow-tools"
171
+ name: "@patwoodman/dsh-sdlc-flow-tools/report"
156
172
  - id: preset-sdlc-data-engineer
157
173
  name: "@deepseek-ai/dsh-agent-preset"
158
174
  config:
@@ -172,8 +188,10 @@
172
188
  name: "@deepseek-ai/dsh-tool-fs"
173
189
  - id: tool-todo
174
190
  name: "@deepseek-ai/dsh-tool-todo"
191
+ config:
192
+ allowParallelInProgress: false
175
193
  - id: sdlc-report
176
- name: "@patwoodman/dsh-sdlc-flow-tools"
194
+ name: "@patwoodman/dsh-sdlc-flow-tools/report"
177
195
  - id: preset-sdlc-database-engineer
178
196
  name: "@deepseek-ai/dsh-agent-preset"
179
197
  config:
@@ -193,8 +211,10 @@
193
211
  name: "@deepseek-ai/dsh-tool-fs"
194
212
  - id: tool-todo
195
213
  name: "@deepseek-ai/dsh-tool-todo"
214
+ config:
215
+ allowParallelInProgress: false
196
216
  - id: sdlc-report
197
- name: "@patwoodman/dsh-sdlc-flow-tools"
217
+ name: "@patwoodman/dsh-sdlc-flow-tools/report"
198
218
  - id: preset-sdlc-ux-designer
199
219
  name: "@deepseek-ai/dsh-agent-preset"
200
220
  config:
@@ -214,8 +234,10 @@
214
234
  name: "@deepseek-ai/dsh-tool-fs"
215
235
  - id: tool-todo
216
236
  name: "@deepseek-ai/dsh-tool-todo"
237
+ config:
238
+ allowParallelInProgress: false
217
239
  - id: sdlc-report
218
- name: "@patwoodman/dsh-sdlc-flow-tools"
240
+ name: "@patwoodman/dsh-sdlc-flow-tools/report"
219
241
  - id: preset-sdlc-ui-designer
220
242
  name: "@deepseek-ai/dsh-agent-preset"
221
243
  config:
@@ -235,8 +257,10 @@
235
257
  name: "@deepseek-ai/dsh-tool-fs"
236
258
  - id: tool-todo
237
259
  name: "@deepseek-ai/dsh-tool-todo"
260
+ config:
261
+ allowParallelInProgress: false
238
262
  - id: sdlc-report
239
- name: "@patwoodman/dsh-sdlc-flow-tools"
263
+ name: "@patwoodman/dsh-sdlc-flow-tools/report"
240
264
  - id: preset-sdlc-accessibility-engineer
241
265
  name: "@deepseek-ai/dsh-agent-preset"
242
266
  config:
@@ -256,8 +280,10 @@
256
280
  name: "@deepseek-ai/dsh-tool-fs"
257
281
  - id: tool-todo
258
282
  name: "@deepseek-ai/dsh-tool-todo"
283
+ config:
284
+ allowParallelInProgress: false
259
285
  - id: sdlc-report
260
- name: "@patwoodman/dsh-sdlc-flow-tools"
286
+ name: "@patwoodman/dsh-sdlc-flow-tools/report"
261
287
  - id: preset-sdlc-frontend-engineer
262
288
  name: "@deepseek-ai/dsh-agent-preset"
263
289
  config:
@@ -277,8 +303,10 @@
277
303
  name: "@deepseek-ai/dsh-tool-fs"
278
304
  - id: tool-todo
279
305
  name: "@deepseek-ai/dsh-tool-todo"
306
+ config:
307
+ allowParallelInProgress: false
280
308
  - id: sdlc-report
281
- name: "@patwoodman/dsh-sdlc-flow-tools"
309
+ name: "@patwoodman/dsh-sdlc-flow-tools/report"
282
310
  - id: preset-sdlc-backend-engineer
283
311
  name: "@deepseek-ai/dsh-agent-preset"
284
312
  config:
@@ -298,8 +326,10 @@
298
326
  name: "@deepseek-ai/dsh-tool-fs"
299
327
  - id: tool-todo
300
328
  name: "@deepseek-ai/dsh-tool-todo"
329
+ config:
330
+ allowParallelInProgress: false
301
331
  - id: sdlc-report
302
- name: "@patwoodman/dsh-sdlc-flow-tools"
332
+ name: "@patwoodman/dsh-sdlc-flow-tools/report"
303
333
  - id: preset-sdlc-api-engineer
304
334
  name: "@deepseek-ai/dsh-agent-preset"
305
335
  config:
@@ -319,8 +349,10 @@
319
349
  name: "@deepseek-ai/dsh-tool-fs"
320
350
  - id: tool-todo
321
351
  name: "@deepseek-ai/dsh-tool-todo"
352
+ config:
353
+ allowParallelInProgress: false
322
354
  - id: sdlc-report
323
- name: "@patwoodman/dsh-sdlc-flow-tools"
355
+ name: "@patwoodman/dsh-sdlc-flow-tools/report"
324
356
  - id: preset-sdlc-integration-engineer
325
357
  name: "@deepseek-ai/dsh-agent-preset"
326
358
  config:
@@ -340,8 +372,10 @@
340
372
  name: "@deepseek-ai/dsh-tool-fs"
341
373
  - id: tool-todo
342
374
  name: "@deepseek-ai/dsh-tool-todo"
375
+ config:
376
+ allowParallelInProgress: false
343
377
  - id: sdlc-report
344
- name: "@patwoodman/dsh-sdlc-flow-tools"
378
+ name: "@patwoodman/dsh-sdlc-flow-tools/report"
345
379
  - id: preset-sdlc-devops-engineer
346
380
  name: "@deepseek-ai/dsh-agent-preset"
347
381
  config:
@@ -361,8 +395,10 @@
361
395
  name: "@deepseek-ai/dsh-tool-fs"
362
396
  - id: tool-todo
363
397
  name: "@deepseek-ai/dsh-tool-todo"
398
+ config:
399
+ allowParallelInProgress: false
364
400
  - id: sdlc-report
365
- name: "@patwoodman/dsh-sdlc-flow-tools"
401
+ name: "@patwoodman/dsh-sdlc-flow-tools/report"
366
402
  - id: preset-sdlc-platform-engineer
367
403
  name: "@deepseek-ai/dsh-agent-preset"
368
404
  config:
@@ -382,8 +418,10 @@
382
418
  name: "@deepseek-ai/dsh-tool-fs"
383
419
  - id: tool-todo
384
420
  name: "@deepseek-ai/dsh-tool-todo"
421
+ config:
422
+ allowParallelInProgress: false
385
423
  - id: sdlc-report
386
- name: "@patwoodman/dsh-sdlc-flow-tools"
424
+ name: "@patwoodman/dsh-sdlc-flow-tools/report"
387
425
  - id: preset-sdlc-qa-engineer
388
426
  name: "@deepseek-ai/dsh-agent-preset"
389
427
  config:
@@ -403,8 +441,10 @@
403
441
  name: "@deepseek-ai/dsh-tool-fs"
404
442
  - id: tool-todo
405
443
  name: "@deepseek-ai/dsh-tool-todo"
444
+ config:
445
+ allowParallelInProgress: false
406
446
  - id: sdlc-report
407
- name: "@patwoodman/dsh-sdlc-flow-tools"
447
+ name: "@patwoodman/dsh-sdlc-flow-tools/report"
408
448
  - id: preset-sdlc-test-automation-engineer
409
449
  name: "@deepseek-ai/dsh-agent-preset"
410
450
  config:
@@ -424,8 +464,10 @@
424
464
  name: "@deepseek-ai/dsh-tool-fs"
425
465
  - id: tool-todo
426
466
  name: "@deepseek-ai/dsh-tool-todo"
467
+ config:
468
+ allowParallelInProgress: false
427
469
  - id: sdlc-report
428
- name: "@patwoodman/dsh-sdlc-flow-tools"
470
+ name: "@patwoodman/dsh-sdlc-flow-tools/report"
429
471
  - id: preset-sdlc-e2e-engineer
430
472
  name: "@deepseek-ai/dsh-agent-preset"
431
473
  config:
@@ -445,8 +487,10 @@
445
487
  name: "@deepseek-ai/dsh-tool-fs"
446
488
  - id: tool-todo
447
489
  name: "@deepseek-ai/dsh-tool-todo"
490
+ config:
491
+ allowParallelInProgress: false
448
492
  - id: sdlc-report
449
- name: "@patwoodman/dsh-sdlc-flow-tools"
493
+ name: "@patwoodman/dsh-sdlc-flow-tools/report"
450
494
  - id: preset-sdlc-visual-qa-engineer
451
495
  name: "@deepseek-ai/dsh-agent-preset"
452
496
  config:
@@ -466,8 +510,10 @@
466
510
  name: "@deepseek-ai/dsh-tool-fs"
467
511
  - id: tool-todo
468
512
  name: "@deepseek-ai/dsh-tool-todo"
513
+ config:
514
+ allowParallelInProgress: false
469
515
  - id: sdlc-report
470
- name: "@patwoodman/dsh-sdlc-flow-tools"
516
+ name: "@patwoodman/dsh-sdlc-flow-tools/report"
471
517
  - id: preset-sdlc-performance-engineer
472
518
  name: "@deepseek-ai/dsh-agent-preset"
473
519
  config:
@@ -487,8 +533,10 @@
487
533
  name: "@deepseek-ai/dsh-tool-fs"
488
534
  - id: tool-todo
489
535
  name: "@deepseek-ai/dsh-tool-todo"
536
+ config:
537
+ allowParallelInProgress: false
490
538
  - id: sdlc-report
491
- name: "@patwoodman/dsh-sdlc-flow-tools"
539
+ name: "@patwoodman/dsh-sdlc-flow-tools/report"
492
540
  - id: preset-sdlc-technical-writer
493
541
  name: "@deepseek-ai/dsh-agent-preset"
494
542
  config:
@@ -508,8 +556,10 @@
508
556
  name: "@deepseek-ai/dsh-tool-fs"
509
557
  - id: tool-todo
510
558
  name: "@deepseek-ai/dsh-tool-todo"
559
+ config:
560
+ allowParallelInProgress: false
511
561
  - id: sdlc-report
512
- name: "@patwoodman/dsh-sdlc-flow-tools"
562
+ name: "@patwoodman/dsh-sdlc-flow-tools/report"
513
563
  - id: preset-sdlc-release-manager
514
564
  name: "@deepseek-ai/dsh-agent-preset"
515
565
  config:
@@ -529,8 +579,10 @@
529
579
  name: "@deepseek-ai/dsh-tool-fs"
530
580
  - id: tool-todo
531
581
  name: "@deepseek-ai/dsh-tool-todo"
582
+ config:
583
+ allowParallelInProgress: false
532
584
  - id: sdlc-report
533
- name: "@patwoodman/dsh-sdlc-flow-tools"
585
+ name: "@patwoodman/dsh-sdlc-flow-tools/report"
534
586
  - id: preset-sdlc-code-reviewer
535
587
  name: "@deepseek-ai/dsh-agent-preset"
536
588
  config:
@@ -550,8 +602,10 @@
550
602
  name: "@deepseek-ai/dsh-tool-fs"
551
603
  - id: tool-todo
552
604
  name: "@deepseek-ai/dsh-tool-todo"
605
+ config:
606
+ allowParallelInProgress: false
553
607
  - id: sdlc-report
554
- name: "@patwoodman/dsh-sdlc-flow-tools"
608
+ name: "@patwoodman/dsh-sdlc-flow-tools/report"
555
609
  - id: preset-sdlc-sre-engineer
556
610
  name: "@deepseek-ai/dsh-agent-preset"
557
611
  config:
@@ -571,8 +625,10 @@
571
625
  name: "@deepseek-ai/dsh-tool-fs"
572
626
  - id: tool-todo
573
627
  name: "@deepseek-ai/dsh-tool-todo"
628
+ config:
629
+ allowParallelInProgress: false
574
630
  - id: sdlc-report
575
- name: "@patwoodman/dsh-sdlc-flow-tools"
631
+ name: "@patwoodman/dsh-sdlc-flow-tools/report"
576
632
  - id: sdlc-presets
577
633
  name: "@patwoodman/dsh-sdlc-flow-presets/plugin"
578
634
  config: {}
@@ -15,19 +15,37 @@
15
15
  * "no unnecessary agents" is structural, not heuristic.
16
16
  */
17
17
 
18
- /** Tools every worker preset composes. Worker presets get NO delegation tools. */
18
+ /**
19
+ * Tools every worker preset composes. Worker presets get NO delegation tools.
20
+ *
21
+ * DSH requires the tool configs in these rows explicitly — neither plugin has
22
+ * a default — and an omitted value fails the WHOLE preset at mount:
23
+ * - `tool-todo`: `allowParallelInProgress` is a required deployment policy.
24
+ * A worker owns exactly one task contract at a time, so it keeps the
25
+ * single-active discipline.
26
+ * - `tool-subagent` (Lead only, below): `provider` is required.
27
+ */
19
28
  export const WORKER_TOOL_ROWS = [
20
29
  { id: 'tool-bash', name: '@deepseek-ai/dsh-tool-bash' },
21
30
  { id: 'tool-fs', name: '@deepseek-ai/dsh-tool-fs' },
22
- { id: 'tool-todo', name: '@deepseek-ai/dsh-tool-todo' },
23
- { id: 'sdlc-report', name: '@patwoodman/dsh-sdlc-flow-tools' },
31
+ { id: 'tool-todo', name: '@deepseek-ai/dsh-tool-todo', config: { allowParallelInProgress: false } },
32
+ { id: 'sdlc-report', name: '@patwoodman/dsh-sdlc-flow-tools/report' },
24
33
  ];
25
34
 
26
- /** Tools only the Lead composes: delegation, plan deltas, decision ledger. */
35
+ /** Tools only the Lead composes: delegation, plan deltas, decision ledger.
36
+ *
37
+ * `provider: 'spawn'` is the in-process backend the web template registers;
38
+ * `backgroundMode: 'continuable'` keeps delegated child conversations available
39
+ * for follow-up turns. Both match DSH's own `standard`/web preset.
40
+ */
27
41
  export const LEAD_TOOL_ROWS = [
28
- { id: 'tool-subagent', name: '@deepseek-ai/dsh-tool-subagent' },
29
- { id: 'sdlc-dispatch', name: '@patwoodman/dsh-sdlc-flow-tools' },
30
- { id: 'sdlc-plan', name: '@patwoodman/dsh-sdlc-flow-tools' },
42
+ {
43
+ id: 'tool-subagent',
44
+ name: '@deepseek-ai/dsh-tool-subagent',
45
+ config: { provider: 'spawn', toolName: 'subagent', backgroundMode: 'continuable' },
46
+ },
47
+ { id: 'sdlc-dispatch', name: '@patwoodman/dsh-sdlc-flow-tools/dispatch' },
48
+ { id: 'sdlc-plan', name: '@patwoodman/dsh-sdlc-flow-tools/plan' },
31
49
  ];
32
50
 
33
51
  export const POLICY_SUFFIX = [
@@ -140,13 +158,12 @@ export function presetConfigIdFor(roleId) {
140
158
  * waiting for service: agentPresets` while the presets silently never
141
159
  * mounted.
142
160
  *
143
- * The row is FIRST inside the presets patch's insert: list so it exists
144
- * before the presets register against it. Two constraints on its config:
145
- * (1) `default` must name a preset that EXISTS in the composed profile —
146
- * `standard` exists on web (shipped row) but on sdk/headless nothing ships
147
- * presets, so the default is our own `sdlc-lead`; (2) on `web` the composed
148
- * profile then holds two same-id rows pointing at the same service — the
149
- * loader accepts both and every preset activates (verified live).
161
+ * The row is FIRST inside the member patch's insert: list so it exists before
162
+ * the presets register against it on headless/sdk profiles, which ship no
163
+ * registry row. On web the shipped row exists, so the generated lead
164
+ * meta-bundle replaces this insert with an id-targeted override of the
165
+ * shipped row's `default` — a duplicate id+name is loader-accepted but blocks
166
+ * the plugin manager's uninstall audit.
150
167
  */
151
168
  export function presetRegistryRow() {
152
169
  return {
@@ -169,6 +186,19 @@ export function personaPrefix(role) {
169
186
  ].join('\n');
170
187
  }
171
188
 
189
+ /**
190
+ * The Lead fans work out to workers/subagents, so its own todo list may legitimately
191
+ * track several active items at once. Workers stay single-active.
192
+ */
193
+ function leadToolRows() {
194
+ const rows = WORKER_TOOL_ROWS.map((row) =>
195
+ row.id === 'tool-todo'
196
+ ? { ...row, config: { ...row.config, allowParallelInProgress: true } }
197
+ : row,
198
+ );
199
+ return [...rows, ...LEAD_TOOL_ROWS];
200
+ }
201
+
172
202
  /** Build the `- insert:` row for one role preset. */
173
203
  export function presetRow(role) {
174
204
  const plugins = [
@@ -180,7 +210,7 @@ export function presetRow(role) {
180
210
  suffix: 'Your working directory is {{cwd}}. Model: {{model}}.',
181
211
  },
182
212
  },
183
- ...(role.lead ? [...WORKER_TOOL_ROWS, ...LEAD_TOOL_ROWS] : WORKER_TOOL_ROWS),
213
+ ...(role.lead ? leadToolRows() : WORKER_TOOL_ROWS),
184
214
  ];
185
215
  return {
186
216
  id: presetIdFor(role.id),
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@patwoodman/dsh-sdlc-flow-presets",
3
- "version": "0.4.0-beta.19",
3
+ "version": "0.4.0-beta.21",
4
4
  "type": "module"
5
5
  }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * packages/sdlc-tools/src/dispatch.js — the Lead's dispatch/approval slice
3
+ * (spec v3 C.3/C.4): propose a dispatch, approve a transition, record a
4
+ * decision. `sdlc_plan_delta` has its own `./plan` entry.
5
+ */
6
+
7
+ import { DISPATCH_TOOLS } from './index.js';
8
+ import { toolSubsetPlugin } from './plugin.js';
9
+
10
+ export const name = 'sdlc-tools/dispatch';
11
+ export const apply = toolSubsetPlugin(DISPATCH_TOOLS);
@@ -16,6 +16,17 @@ export const WORKER_TOOLS = ['sdlc_report_result', 'sdlc_claim_check', 'sdlc_req
16
16
  export const LEAD_TOOLS = ['sdlc_dispatch_task', 'sdlc_approve_transition', 'sdlc_record_decision', 'sdlc_plan_delta'];
17
17
  export const ALL_TOOLS = [...WORKER_TOOLS, ...LEAD_TOOLS];
18
18
 
19
+ /**
20
+ * The three preset-plane slices (spec C.3). Each is a real loader entry with
21
+ * its own subpath export — the preset rows name them directly, so a worker's
22
+ * catalog is the report slice and the Lead's is report + dispatch + plan.
23
+ * They register TOOLS ONLY: the /sdlc command surface belongs to the host
24
+ * `sdlc-tools` row, not to every mounted preset.
25
+ */
26
+ export const REPORT_TOOLS = [...WORKER_TOOLS];
27
+ export const DISPATCH_TOOLS = LEAD_TOOLS.filter((name) => name !== 'sdlc_plan_delta');
28
+ export const PLAN_TOOLS = ['sdlc_plan_delta'];
29
+
19
30
  export const COMMANDS = [
20
31
  'run', 'status', 'tasks', 'workers', 'models', 'logs', 'verify', 'resume', 'stop',
21
32
  'explain', 'retry', 'elevate', 'approve', 'reject', 'doctor', 'config', 'init', 'lock', 'selftest',
@@ -227,4 +238,4 @@ export function validateToolArgs(name, args = {}) {
227
238
  return errors.length ? { ok: false, error: errors.join('; ') } : { ok: true };
228
239
  }
229
240
 
230
- export default { TOOL_DEFS, WORKER_TOOLS, LEAD_TOOLS, COMMANDS, createHandlers, writeOutbox };
241
+ export default { TOOL_DEFS, WORKER_TOOLS, LEAD_TOOLS, REPORT_TOOLS, DISPATCH_TOOLS, PLAN_TOOLS, COMMANDS, createHandlers, writeOutbox };
@@ -0,0 +1,11 @@
1
+ /**
2
+ * packages/sdlc-tools/src/plan.js — the Lead's plan-delta slice (spec v3
3
+ * C.3/C.4). Kept separate so the preset row can name exactly the tool it
4
+ * grants instead of re-mounting the whole tools plugin.
5
+ */
6
+
7
+ import { PLAN_TOOLS } from './index.js';
8
+ import { toolSubsetPlugin } from './plugin.js';
9
+
10
+ export const name = 'sdlc-tools/plan';
11
+ export const apply = toolSubsetPlugin(PLAN_TOOLS);
@@ -18,13 +18,20 @@ function pkgVersion() {
18
18
  }
19
19
  }
20
20
 
21
- /** Register every tool definition on a DSH context. */
21
+ /**
22
+ * Register tool definitions on a DSH context.
23
+ *
24
+ * `config.tools` narrows the set to those names (the preset slices); omitting
25
+ * it registers the complete surface (the host `sdlc-tools` row).
26
+ */
22
27
  export function registerTools(ctx, config = {}) {
23
28
  const registered = [];
29
+ const selected = Array.isArray(config.tools) ? new Set(config.tools) : null;
24
30
  const worktreePath = config.worktreePath ?? process.cwd();
25
31
  const handlers = createHandlers({ worktreePath, runMeta: config.runMeta ?? {} });
26
32
 
27
33
  for (const [name, def] of Object.entries(TOOL_DEFS)) {
34
+ if (selected !== null && !selected.has(name)) continue;
28
35
  const handler = handlers[name];
29
36
  if (!handler) continue;
30
37
  registered.push(name);
@@ -67,6 +74,24 @@ export function registerCommands(ctx, config = {}) {
67
74
  return registered;
68
75
  }
69
76
 
77
+ /**
78
+ * Build a loader entry for one preset-plane tool slice.
79
+ *
80
+ * The preset rows use the package's `./report`, `./dispatch` and `./plan`
81
+ * exports, each of which is one of these. A slice registers only its tools
82
+ * and NEVER the command surface: the host `sdlc-tools` row owns commands, and
83
+ * mounting them once per preset would collide on the second mount.
84
+ * @param {string[]} toolNames - the exact tool names this entry registers.
85
+ * @returns {(ctx: object, config?: object) => undefined}
86
+ */
87
+ export function toolSubsetPlugin(toolNames) {
88
+ const tools = [...toolNames];
89
+ return function applyToolSubset(ctx, config = {}) {
90
+ setSelftestMeta({ version: pkgVersion() });
91
+ contributeSelftest({ tools: registerTools(ctx, { ...config, tools }) });
92
+ };
93
+ }
94
+
70
95
  /**
71
96
  * Loader entry. MUST return nothing: the Cordis runner interprets a plugin's
72
97
  * return value as an effect (`TypeError: Invalid effect` for a
@@ -0,0 +1,15 @@
1
+ /**
2
+ * packages/sdlc-tools/src/report.js — the worker preset's propose-only result
3
+ * slice (spec v3 C.3/C.4).
4
+ *
5
+ * The preset rows name this subpath, not the package root: `lib/index.js` is
6
+ * data only, so the loader would reject it with "never started". Each slice is
7
+ * a real loader entry and registers tools only — commands stay with the host
8
+ * `sdlc-tools` row.
9
+ */
10
+
11
+ import { REPORT_TOOLS } from './index.js';
12
+ import { toolSubsetPlugin } from './plugin.js';
13
+
14
+ export const name = 'sdlc-tools/report';
15
+ export const apply = toolSubsetPlugin(REPORT_TOOLS);
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@patwoodman/dsh-sdlc-flow-tools",
3
- "version": "0.4.0-beta.19",
3
+ "version": "0.4.0-beta.21",
4
4
  "type": "module"
5
5
  }
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@patwoodman/dsh-sdlc-flow-ui",
3
- "version": "0.4.0-beta.19",
3
+ "version": "0.4.0-beta.21",
4
4
  "type": "module"
5
5
  }
package/package.json CHANGED
@@ -1,11 +1,15 @@
1
1
  {
2
2
  "name": "@patwoodman/dsh-sdlc-flow",
3
- "version": "0.4.0-beta.19",
3
+ "version": "0.4.0-beta.21",
4
4
  "type": "module",
5
5
  "description": "DSH SDLC Flow lead-profile bundle: task engine, tools, role presets, and web UI in one install.",
6
6
  "main": "lib/index.js",
7
+ "bin": {
8
+ "dsh-sdlc-install": "lib/install.mjs"
9
+ },
7
10
  "exports": {
8
11
  ".": "./lib/index.js",
12
+ "./install": "./lib/install.mjs",
9
13
  "./package.json": "./package.json",
10
14
  "./locale/*.json": "./locale/*.json",
11
15
  "./icon": "./icon.svg",
@@ -16,6 +20,9 @@
16
20
  "./selftest": "./lib/rows/core/lib/selftest.js",
17
21
  "./tools": "./lib/rows/tools/lib/index.js",
18
22
  "./tools-plugin": "./lib/rows/tools/lib/plugin.js",
23
+ "./tools-report": "./lib/rows/tools/lib/report.js",
24
+ "./tools-dispatch": "./lib/rows/tools/lib/dispatch.js",
25
+ "./tools-plan": "./lib/rows/tools/lib/plan.js",
19
26
  "./presets": "./lib/rows/presets/lib/index.js",
20
27
  "./presets-plugin": "./lib/rows/presets/lib/plugin.js",
21
28
  "./ui": "./lib/rows/ui/lib/index.js",