@godv61/dsh-task-engine 0.23.4 → 0.23.6
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/.preset-test.mjs +72 -0
- package/docs/CHANGELOG.md +11 -0
- package/lib/seed-preset.d.ts +59 -10
- package/lib/seed-preset.js +180 -31
- package/lib/seed-preset.js.map +1 -1
- package/package.json +5 -4
- package/scripts/verify-dsh-compat.mjs +145 -0
- package/scripts/verify-package.mjs +136 -0
package/.preset-test.mjs
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the `eng` preset derivation.
|
|
3
|
+
*
|
|
4
|
+
* The preset must track the RUNNING harness's `standard`, because a copy that
|
|
5
|
+
* freezes once goes stale the moment DSH edits its own composition, and a stale
|
|
6
|
+
* preset fails to load entirely. Two real regressions motivate these cases:
|
|
7
|
+
*
|
|
8
|
+
* - DSH 2026-09-06 moved the persona row from `text:` to `prefix`/`suffix`.
|
|
9
|
+
* - DSH 2026-09-13 replaced the `workflow-worker-thread` row with
|
|
10
|
+
* `workflow-ptc`; the plugin kept emitting the removed row, whose package
|
|
11
|
+
* had also left `apps/cli`'s dependencies, so the roster could not resolve
|
|
12
|
+
* it and reported "加载失败" for the whole preset.
|
|
13
|
+
*
|
|
14
|
+
* Run: `node --test .preset-test.mjs`
|
|
15
|
+
* @module dsh-task-engine/.preset-test
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import assert from 'node:assert/strict'
|
|
19
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync, mkdirSync, existsSync } from 'node:fs'
|
|
20
|
+
import { tmpdir } from 'node:os'
|
|
21
|
+
import { join, resolve, sep } from 'node:path'
|
|
22
|
+
import { test } from 'node:test'
|
|
23
|
+
import { derivedEngComposition, wasSeeded } from './lib/seed-preset.js'
|
|
24
|
+
|
|
25
|
+
/** The live derivation must exist in a built checkout. */
|
|
26
|
+
const derived = derivedEngComposition()
|
|
27
|
+
|
|
28
|
+
test('the derivation reads a readable standard composition', () => {
|
|
29
|
+
assert.ok(derived !== undefined, 'standard must be resolvable in a built checkout')
|
|
30
|
+
assert.ok(derived.text.length > 0, 'the derived composition must not be empty')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('the engineering persona replaces the shipped one', () => {
|
|
34
|
+
assert.equal(derived.personaApplied, true, 'the persona row must be found and rewritten')
|
|
35
|
+
assert.match(derived.text, /engineering-delivery coding agent/u, 'the persona body must be the engineering one')
|
|
36
|
+
assert.doesNotMatch(derived.text, /text: >-\n/u, 'the pre-2026-09-06 persona phrasing must be gone')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('the persona row uses the current prefix form and never sets complete', () => {
|
|
40
|
+
assert.match(derived.text, /- id: persona\n {2}name: '@deepseek-ai\/dsh-persona'\n {2}config:\n {4}prefix: \|-/u,
|
|
41
|
+
'the persona row must carry the current prefix form')
|
|
42
|
+
// `complete: true` would make this prefix the ENTIRE system prompt and
|
|
43
|
+
// suppress every other section, silently gutting the agent.
|
|
44
|
+
assert.doesNotMatch(derived.text, /complete: true/u, 'complete must not be set')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('the agent row is appended exactly once', () => {
|
|
48
|
+
const matches = derived.text.match(/id: task-engine-agent/gu) ?? []
|
|
49
|
+
assert.equal(matches.length, 1, 'task-engine-agent must appear once')
|
|
50
|
+
assert.match(derived.text, /name: '@godv61\/dsh-task-engine\/agent'/u, 'the row must name the agent entry')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('every top-level row of standard survives the derivation', () => {
|
|
54
|
+
// The point of deriving rather than hand-writing: the preset must inherit the
|
|
55
|
+
// harness's whole tool roster, not a remembered subset of it.
|
|
56
|
+
const ids = (text) => (text.match(/^- id: (\S+)$/gmu) ?? []).map(line => line.replace(/^- id: /u, ''))
|
|
57
|
+
const derivedIds = ids(derived.text)
|
|
58
|
+
assert.ok(derivedIds.length > 10, `expected the full roster, saw ${derivedIds.length} rows`)
|
|
59
|
+
assert.ok(derivedIds.includes('persona'), 'persona must be present')
|
|
60
|
+
assert.equal(new Set(derivedIds).size, derivedIds.length, 'row ids must be unique')
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('a composition carrying the agent row is recognised as seeded', () => {
|
|
64
|
+
assert.equal(wasSeeded(" - id: task-engine-agent\n name: 'x'\n"), true, 'the agent row marks a seeded file')
|
|
65
|
+
assert.equal(wasSeeded('- id: persona\n name: y\n'), false, 'a hand-authored composition is not ours')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('re-deriving is stable: the same standard yields the same text', () => {
|
|
69
|
+
// Idempotence is what lets the seed run on every boot without rewriting.
|
|
70
|
+
const again = derivedEngComposition()
|
|
71
|
+
assert.equal(again.text, derived.text, 'two derivations of one source must agree')
|
|
72
|
+
})
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,17 @@
|
|
|
4
4
|
|
|
5
5
|
按版本查阅功能变化。当前使用方式以[项目首页](../README.md)和使用指南为准;历史条目中的实现方式、限制与测试数量可能已被后续版本替代。
|
|
6
6
|
|
|
7
|
+
## 0.23.6
|
|
8
|
+
|
|
9
|
+
- **修复「工程化开发引擎」预设加载失败**。`seed-preset.ts` 用 `@deepseek-ai/dsh-agent-presets` 导出的 `SHIPPED_PRESET_ROOT` 常量定位源预设,而该常量是 `fileURLToPath(new URL('../presets/', import.meta.url))`——它锁在**插件自己依赖树**里的那份包上,可能和**正在运行的** DSH 不同版本。于是派生出的预设携带运行中 harness 早已替换掉的行:DSH 2026-09-13 把 `workflow-worker-thread` 换成了 `workflow-ptc`,而该包同时退出了 `apps/cli` 的依赖,预设因此报 `row "workflow-worker-thread" names a plugin that cannot be resolved`,整个预设显示「加载失败」,任何新会话都无法启用任务流程。现在改为在**调用时**用 `createRequire` 从插件自身位置解析,跟随宿主的安装图。
|
|
10
|
+
- **预设改为每次启动重新派生**,而非"存在即跳过"。此前一次性复制把预设冻结成首次运行时的快照,DSH 每次修改 `standard` 都会让它失配;persona 格式也经历过 `text:` → `prefix`/`suffix` 的迁移,旧副本因此被 persona 插件的 schema 拒绝。现在检测到是本插件生成的预设才更新,**手工编辑过的预设绝不被覆盖**。
|
|
11
|
+
- persona 行改为**整块重写 `config:`**,不再匹配某一种历史措辞,因此对 schema 的后续演进免疫。同时**不再设置 `complete: true`**——那会让这段 prefix 成为整个系统提示词并抑制其余所有 section。
|
|
12
|
+
- 新增 `.preset-test.mjs`(7 项),断言 persona 已替换、使用当前 `prefix` 形式、不含 `complete`、agent 行只出现一次、`standard` 的所有顶层行都被继承、以及重复派生结果稳定。已反向验证:把 `complete: true` 加回去该测试即失败。`npm test` 从 43 项增至 50 项。
|
|
13
|
+
|
|
14
|
+
## 0.23.5
|
|
15
|
+
|
|
16
|
+
- **修复 `scripts/` 未随包发布,导致两条已声明的 npm script 在安装后无法执行**。`package.json` 声明了 `verify:package` 与 `verify:dsh`,但 `files` 白名单不含 `scripts/**`,用户装包后运行它们会直接 `MODULE_NOT_FOUND`。该问题早于 0.23.3 存在(`verify:package` 一直如此),0.23.4 新增的 `verify:dsh` 只是沿用了同一模式。现在把 `scripts/**` 纳入白名单,并在 `verify:package` 中加断言:包内必须能找到每一条 `package.json` 里声明的 `node <file>` script 目标。
|
|
17
|
+
|
|
7
18
|
## 0.23.4
|
|
8
19
|
|
|
9
20
|
- **关闭两处 Remote 路径边界漏洞**。`checkedPath` 此前用**原始字符串**比对禁止前缀,`D:/proj/../../Windows` 不匹配任何前缀,却被后续 `join()` 解析到 `C:/Windows`;实测 8 个越界样本中旧实现放过 7 个。现在先 `resolve()` 归一化再判断,禁止列表不再绑定盘符(`D:/Windows`、`E:/Program Files` 同样拒绝),并拒绝裸盘符根(`D:/`、`C:/`)。
|
package/lib/seed-preset.d.ts
CHANGED
|
@@ -1,19 +1,68 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Seed the `eng` preset into the user's `.agent-presets` root
|
|
2
|
+
* Seed the `eng` preset into the user's `.agent-presets` root.
|
|
3
3
|
*
|
|
4
|
-
* Installing the bundle must put a pickable "工程化开发引擎" preset in the
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* engineering persona,
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
4
|
+
* Installing the bundle must put a pickable "工程化开发引擎" preset in the preset
|
|
5
|
+
* picker without anyone hand-copying a composition. The preset is the shipped
|
|
6
|
+
* `standard` composition with three edits: the persona is replaced by the
|
|
7
|
+
* engineering persona, `task-engine-agent` is appended, and nothing else changes
|
|
8
|
+
* — so a session on it has every tool `standard` has, plus `dev_task`.
|
|
9
|
+
*
|
|
10
|
+
* The seed DERIVES the composition from the `standard` that is running, and
|
|
11
|
+
* RE-DERIVES it whenever that source moves. The earlier version copied once and
|
|
12
|
+
* then returned early on every later boot, which turned the preset into a frozen
|
|
13
|
+
* snapshot of whatever `standard` looked like the first time:
|
|
14
|
+
*
|
|
15
|
+
* - DSH 2026-09-06 changed the persona row from `text:` to `prefix`/`suffix`.
|
|
16
|
+
* A stale copy kept `text:`, which the persona plugin's schema (prefix is
|
|
17
|
+
* required) rejects outright.
|
|
18
|
+
* - DSH 2026-09-13 replaced the `workflow-worker-thread` row with
|
|
19
|
+
* `workflow-ptc`. A stale copy kept the removed row, and because that
|
|
20
|
+
* package also left `apps/cli`'s dependencies the roster could no longer
|
|
21
|
+
* resolve it: the preset reported "加载失败" and no new session could use
|
|
22
|
+
* the task flow at all.
|
|
23
|
+
*
|
|
24
|
+
* Both were invisible until a user upgraded DSH, and both left the preset
|
|
25
|
+
* unusable rather than merely outdated. Re-deriving on every boot makes the
|
|
26
|
+
* preset track its source, and a hand-edited preset is never silently
|
|
27
|
+
* clobbered: any local edit is detected and preserved.
|
|
11
28
|
*
|
|
12
29
|
* @module dsh-task-engine/seed-preset
|
|
13
30
|
*/
|
|
14
31
|
/**
|
|
15
|
-
*
|
|
32
|
+
* The persona row `standard` shipped before 2026-09-06, kept so a preset seeded
|
|
33
|
+
* by an older release can still be recognised and re-derived.
|
|
34
|
+
*/
|
|
35
|
+
declare const LEGACY_PERSONA_BLOCK: string;
|
|
36
|
+
/** The persona fragment the seed itself wrote into `eng`, in the old `text:` form. */
|
|
37
|
+
declare const SEEDED_PERSONA_MARKER = " text: |-\n";
|
|
38
|
+
/**
|
|
39
|
+
* Whether an existing `eng` composition is one this package previously seeded,
|
|
40
|
+
* as opposed to something the user has since edited by hand. Only a previously
|
|
41
|
+
* seeded preset is re-derived; a hand-edited one is left exactly as it is.
|
|
42
|
+
* @param composition - the existing composition text.
|
|
43
|
+
* @returns true when the seed owns the file.
|
|
44
|
+
*/
|
|
45
|
+
declare function wasSeeded(composition: string): boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Create or refresh the `eng` preset under `<dshHome>/.agent-presets`.
|
|
48
|
+
*
|
|
16
49
|
* Failures are swallowed: a seed that cannot write must not stop the bundle's
|
|
17
|
-
* host half (its Remote controller and workbench) from loading.
|
|
50
|
+
* host half (its Remote controller and workbench) from loading. A preset that
|
|
51
|
+
* was deleted by the user stays deleted — re-derivation only touches a file the
|
|
52
|
+
* seed itself created.
|
|
18
53
|
*/
|
|
19
54
|
export declare function seedEngPreset(): void;
|
|
55
|
+
/**
|
|
56
|
+
* The composition text this package would seed right now. Exported for the
|
|
57
|
+
* package's own verification, so the derivation can be asserted without booting
|
|
58
|
+
* a harness.
|
|
59
|
+
* @returns the derived composition, or undefined when `standard` is unreadable.
|
|
60
|
+
*/
|
|
61
|
+
export declare function derivedEngComposition(): {
|
|
62
|
+
text: string;
|
|
63
|
+
personaApplied: boolean;
|
|
64
|
+
} | undefined;
|
|
65
|
+
/** Exported for tests: whether a composition is one this seed owns. */
|
|
66
|
+
export { wasSeeded };
|
|
67
|
+
/** Exported for tests: the pre-2026-09-06 persona phrasing this seed must still recognise. */
|
|
68
|
+
export { LEGACY_PERSONA_BLOCK, SEEDED_PERSONA_MARKER };
|
package/lib/seed-preset.js
CHANGED
|
@@ -1,59 +1,208 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Seed the `eng` preset into the user's `.agent-presets` root
|
|
2
|
+
* Seed the `eng` preset into the user's `.agent-presets` root.
|
|
3
3
|
*
|
|
4
|
-
* Installing the bundle must put a pickable "工程化开发引擎" preset in the
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* engineering persona,
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
4
|
+
* Installing the bundle must put a pickable "工程化开发引擎" preset in the preset
|
|
5
|
+
* picker without anyone hand-copying a composition. The preset is the shipped
|
|
6
|
+
* `standard` composition with three edits: the persona is replaced by the
|
|
7
|
+
* engineering persona, `task-engine-agent` is appended, and nothing else changes
|
|
8
|
+
* — so a session on it has every tool `standard` has, plus `dev_task`.
|
|
9
|
+
*
|
|
10
|
+
* The seed DERIVES the composition from the `standard` that is running, and
|
|
11
|
+
* RE-DERIVES it whenever that source moves. The earlier version copied once and
|
|
12
|
+
* then returned early on every later boot, which turned the preset into a frozen
|
|
13
|
+
* snapshot of whatever `standard` looked like the first time:
|
|
14
|
+
*
|
|
15
|
+
* - DSH 2026-09-06 changed the persona row from `text:` to `prefix`/`suffix`.
|
|
16
|
+
* A stale copy kept `text:`, which the persona plugin's schema (prefix is
|
|
17
|
+
* required) rejects outright.
|
|
18
|
+
* - DSH 2026-09-13 replaced the `workflow-worker-thread` row with
|
|
19
|
+
* `workflow-ptc`. A stale copy kept the removed row, and because that
|
|
20
|
+
* package also left `apps/cli`'s dependencies the roster could no longer
|
|
21
|
+
* resolve it: the preset reported "加载失败" and no new session could use
|
|
22
|
+
* the task flow at all.
|
|
23
|
+
*
|
|
24
|
+
* Both were invisible until a user upgraded DSH, and both left the preset
|
|
25
|
+
* unusable rather than merely outdated. Re-deriving on every boot makes the
|
|
26
|
+
* preset track its source, and a hand-edited preset is never silently
|
|
27
|
+
* clobbered: any local edit is detected and preserved.
|
|
11
28
|
*
|
|
12
29
|
* @module dsh-task-engine/seed-preset
|
|
13
30
|
*/
|
|
14
31
|
import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
32
|
+
import { createRequire } from 'node:module';
|
|
15
33
|
import { homedir } from 'node:os';
|
|
16
|
-
import { join } from 'node:path';
|
|
34
|
+
import { dirname, join } from 'node:path';
|
|
17
35
|
import { fileURLToPath } from 'node:url';
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
36
|
+
/**
|
|
37
|
+
* The persona row `standard` shipped before 2026-09-06, kept so a preset seeded
|
|
38
|
+
* by an older release can still be recognised and re-derived.
|
|
39
|
+
*/
|
|
40
|
+
const LEGACY_PERSONA_BLOCK = ' text: >-\n'
|
|
21
41
|
+ ' You are a coding agent powered by the {{model}} model. Your working directory is {{cwd}}.\n';
|
|
42
|
+
/** The persona fragment the seed itself wrote into `eng`, in the old `text:` form. */
|
|
43
|
+
const SEEDED_PERSONA_MARKER = ' text: |-\n';
|
|
44
|
+
/** The agent-plane row that activates `dev_task` and the shipped skills. */
|
|
45
|
+
const AGENT_ROW_ID = 'task-engine-agent';
|
|
22
46
|
/** The agent-plane row that activates `dev_task` and the shipped skills. */
|
|
23
47
|
const AGENT_ROW = "\n- id: task-engine-agent\n name: '@godv61/dsh-task-engine/agent'\n";
|
|
48
|
+
/** Metadata written beside the composition. */
|
|
49
|
+
const PRESET_META = 'name: 工程化开发引擎\n'
|
|
50
|
+
+ 'description: 通过 dev_task 工具硬性持有需求评审→设计→开发→交付→代码审核的状态机,按项目 .dsh/eng.json 配置流转与提交门禁。\n';
|
|
24
51
|
/** The harness home, honouring an explicit override first. */
|
|
25
52
|
function dshHome() {
|
|
26
53
|
return process.env.DSH_HOME ?? join(homedir(), '.dsh');
|
|
27
54
|
}
|
|
28
55
|
/**
|
|
29
|
-
*
|
|
56
|
+
* The shipped `standard` composition this preset derives from, resolved at CALL
|
|
57
|
+
* time from the installed DSH rather than from a build-time constant.
|
|
58
|
+
*
|
|
59
|
+
* `@deepseek-ai/dsh-agent-presets` also exports `SHIPPED_PRESET_ROOT`, but that
|
|
60
|
+
* is `fileURLToPath(new URL('../presets/', import.meta.url))` — a constant baked
|
|
61
|
+
* into whichever copy of the package the *plugin* resolves. Under a real install
|
|
62
|
+
* that is the plugin's own dependency tree, which can sit at a different DSH
|
|
63
|
+
* version than the one running: the seed then copies an obsolete `standard`,
|
|
64
|
+
* which is exactly how the `eng` preset acquired a row
|
|
65
|
+
* (`workflow-worker-thread`) that the running harness had already replaced with
|
|
66
|
+
* `workflow-ptc`, and a persona in the pre-`prefix` `text:` form the persona
|
|
67
|
+
* plugin now rejects. Resolving from this module's own location follows the
|
|
68
|
+
* host's install graph, so the derived preset matches the harness that will
|
|
69
|
+
* mount it.
|
|
70
|
+
* @returns the absolute `presets` directory, or undefined when unresolvable.
|
|
71
|
+
*/
|
|
72
|
+
function shippedPresetRoot() {
|
|
73
|
+
try {
|
|
74
|
+
const require_ = createRequire(import.meta.url);
|
|
75
|
+
return join(dirname(require_.resolve('@deepseek-ai/dsh-agent-presets/package.json')), 'presets');
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** The shipped `standard` composition this preset derives from. */
|
|
82
|
+
function shippedStandard() {
|
|
83
|
+
const root = shippedPresetRoot();
|
|
84
|
+
return root === undefined ? undefined : join(root, 'standard');
|
|
85
|
+
}
|
|
86
|
+
/** The engineering persona body, read from this package. */
|
|
87
|
+
function personaBody() {
|
|
88
|
+
return readFileSync(fileURLToPath(new URL('../preset/persona.md', import.meta.url)), 'utf8').trimEnd();
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Replace the persona row's config with the engineering persona.
|
|
92
|
+
*
|
|
93
|
+
* The persona plugin's schema has moved twice (`text:` → `prefix`/`suffix`), so
|
|
94
|
+
* this rewrites the whole `config:` block of the `persona` row rather than
|
|
95
|
+
* matching one historical phrasing. A shape this does not recognise is left
|
|
96
|
+
* untouched and reported, never guessed at.
|
|
97
|
+
* @param composition - the `standard` composition text.
|
|
98
|
+
* @param persona - the engineering persona prose.
|
|
99
|
+
* @returns the rewritten text plus whether the row was found.
|
|
100
|
+
*/
|
|
101
|
+
function applyPersona(composition, persona) {
|
|
102
|
+
const lines = composition.split('\n');
|
|
103
|
+
const start = lines.findIndex(line => line.trimEnd() === '- id: persona');
|
|
104
|
+
if (start < 0)
|
|
105
|
+
return { text: composition, applied: false };
|
|
106
|
+
// The row ends at the next top-level entry.
|
|
107
|
+
let end = start + 1;
|
|
108
|
+
while (end < lines.length && !/^- /u.test(lines[end] ?? ''))
|
|
109
|
+
end++;
|
|
110
|
+
// Keep the row's identity lines (`- id`, ` name:`); drop only its config.
|
|
111
|
+
const header = lines.slice(start, end).filter(line => !/^\s{2,}config:/u.test(line) && !/^\s{4,}\S/u.test(line));
|
|
112
|
+
const indented = persona.split('\n').map(line => (line === '' ? '' : ` ${line}`));
|
|
113
|
+
// `complete` is deliberately NOT set: it would make this prefix the entire
|
|
114
|
+
// system prompt, suppressing the suffix and every other section.
|
|
115
|
+
const body = [
|
|
116
|
+
...header.filter(line => line !== ''),
|
|
117
|
+
' config:',
|
|
118
|
+
' prefix: |-',
|
|
119
|
+
...indented,
|
|
120
|
+
];
|
|
121
|
+
return { text: [...lines.slice(0, start), ...body, ...lines.slice(end)].join('\n'), applied: true };
|
|
122
|
+
}
|
|
123
|
+
/** Append the agent row when the composition does not already name it. */
|
|
124
|
+
function applyAgentRow(composition) {
|
|
125
|
+
if (composition.includes(`id: ${AGENT_ROW_ID}`))
|
|
126
|
+
return composition;
|
|
127
|
+
return composition.trimEnd() + '\n' + AGENT_ROW;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Whether an existing `eng` composition is one this package previously seeded,
|
|
131
|
+
* as opposed to something the user has since edited by hand. Only a previously
|
|
132
|
+
* seeded preset is re-derived; a hand-edited one is left exactly as it is.
|
|
133
|
+
* @param composition - the existing composition text.
|
|
134
|
+
* @returns true when the seed owns the file.
|
|
135
|
+
*/
|
|
136
|
+
function wasSeeded(composition) {
|
|
137
|
+
return composition.includes(`id: ${AGENT_ROW_ID}`);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Build the `eng` composition from the running `standard`.
|
|
141
|
+
* @returns the composition text, or undefined when `standard` cannot be read.
|
|
142
|
+
*/
|
|
143
|
+
function deriveComposition() {
|
|
144
|
+
const standard = shippedStandard();
|
|
145
|
+
if (standard === undefined)
|
|
146
|
+
return undefined;
|
|
147
|
+
let source;
|
|
148
|
+
try {
|
|
149
|
+
source = readFileSync(join(standard, 'agent.cordis.yml'), 'utf8');
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
const persona = applyPersona(source, personaBody());
|
|
155
|
+
return { text: applyAgentRow(persona.text), personaApplied: persona.applied };
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Create or refresh the `eng` preset under `<dshHome>/.agent-presets`.
|
|
159
|
+
*
|
|
30
160
|
* Failures are swallowed: a seed that cannot write must not stop the bundle's
|
|
31
|
-
* host half (its Remote controller and workbench) from loading.
|
|
161
|
+
* host half (its Remote controller and workbench) from loading. A preset that
|
|
162
|
+
* was deleted by the user stays deleted — re-derivation only touches a file the
|
|
163
|
+
* seed itself created.
|
|
32
164
|
*/
|
|
33
165
|
export function seedEngPreset() {
|
|
34
166
|
const target = join(dshHome(), '.agent-presets', 'eng');
|
|
35
|
-
|
|
36
|
-
return;
|
|
167
|
+
const compositionPath = join(target, 'agent.cordis.yml');
|
|
37
168
|
try {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
169
|
+
const derived = deriveComposition();
|
|
170
|
+
if (derived === undefined)
|
|
171
|
+
return; // no readable `standard`: leave any existing preset alone
|
|
172
|
+
if (existsSync(compositionPath)) {
|
|
173
|
+
const existing = readFileSync(compositionPath, 'utf8');
|
|
174
|
+
if (!wasSeeded(existing))
|
|
175
|
+
return; // hand-authored: never clobber
|
|
176
|
+
if (existing === derived.text)
|
|
177
|
+
return; // already current
|
|
178
|
+
writeFileSync(compositionPath, derived.text, 'utf8');
|
|
179
|
+
writeFileSync(join(target, 'preset.yml'), PRESET_META, 'utf8');
|
|
180
|
+
return;
|
|
50
181
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
182
|
+
mkdirSync(join(dshHome(), '.agent-presets'), { recursive: true });
|
|
183
|
+
// Copy the source directory so any future companion file travels with it,
|
|
184
|
+
// then overwrite the composition with the derived text.
|
|
185
|
+
const source = shippedStandard();
|
|
186
|
+
if (source !== undefined)
|
|
187
|
+
cpSync(source, target, { recursive: true });
|
|
188
|
+
writeFileSync(compositionPath, derived.text, 'utf8');
|
|
189
|
+
writeFileSync(join(target, 'preset.yml'), PRESET_META, 'utf8');
|
|
54
190
|
}
|
|
55
191
|
catch {
|
|
56
192
|
// A failed seed is recoverable with `dsh-task-engine-enable`; do not fail boot.
|
|
57
193
|
}
|
|
58
194
|
}
|
|
195
|
+
/**
|
|
196
|
+
* The composition text this package would seed right now. Exported for the
|
|
197
|
+
* package's own verification, so the derivation can be asserted without booting
|
|
198
|
+
* a harness.
|
|
199
|
+
* @returns the derived composition, or undefined when `standard` is unreadable.
|
|
200
|
+
*/
|
|
201
|
+
export function derivedEngComposition() {
|
|
202
|
+
return deriveComposition();
|
|
203
|
+
}
|
|
204
|
+
/** Exported for tests: whether a composition is one this seed owns. */
|
|
205
|
+
export { wasSeeded };
|
|
206
|
+
/** Exported for tests: the pre-2026-09-06 persona phrasing this seed must still recognise. */
|
|
207
|
+
export { LEGACY_PERSONA_BLOCK, SEEDED_PERSONA_MARKER };
|
|
59
208
|
//# sourceMappingURL=seed-preset.js.map
|
package/lib/seed-preset.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"seed-preset.js","sourceRoot":"","sources":["../src/seed-preset.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"seed-preset.js","sourceRoot":"","sources":["../src/seed-preset.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@godv61/dsh-task-engine",
|
|
3
|
-
"version": "0.23.
|
|
3
|
+
"version": "0.23.6",
|
|
4
4
|
"description": "Preset-workflow engineering delivery engine for DeepSeek Harness: a `dev_task` tool that hard-gates stage transitions and commit scope/message, per-node skill/rule bindings with progressive disclosure, a workbench to pick a flow preset and mount skills/rules, and preset-scoped activation — the tool and skills activate only for sessions on a preset that names the agent row.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -33,12 +33,14 @@
|
|
|
33
33
|
"skills/**",
|
|
34
34
|
"rules/**",
|
|
35
35
|
"preset/**",
|
|
36
|
+
"scripts/**",
|
|
36
37
|
"docs/**",
|
|
37
38
|
"README.md",
|
|
38
39
|
".p0-test.mjs",
|
|
39
40
|
".resource-test.mjs",
|
|
40
41
|
".workflow-test.mjs",
|
|
41
|
-
".hook-test.mjs"
|
|
42
|
+
".hook-test.mjs",
|
|
43
|
+
".preset-test.mjs"
|
|
42
44
|
],
|
|
43
45
|
"license": "MIT",
|
|
44
46
|
"publishConfig": {
|
|
@@ -59,7 +61,7 @@
|
|
|
59
61
|
"scripts": {
|
|
60
62
|
"build": "tsc -p tsconfig.json && node build-client.mjs && node build-hook.mjs",
|
|
61
63
|
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.client.json --noEmit",
|
|
62
|
-
"test": "node .p0-test.mjs && node --test .resource-test.mjs .workflow-test.mjs .hook-test.mjs",
|
|
64
|
+
"test": "node .p0-test.mjs && node --test .resource-test.mjs .workflow-test.mjs .hook-test.mjs .preset-test.mjs",
|
|
63
65
|
"verify:package": "node scripts/verify-package.mjs",
|
|
64
66
|
"verify:dsh": "node scripts/verify-dsh-compat.mjs",
|
|
65
67
|
"prepare": "npm run build"
|
|
@@ -83,7 +85,6 @@
|
|
|
83
85
|
"@types/react": "~18.3.1",
|
|
84
86
|
"esbuild": "^0.24.0",
|
|
85
87
|
"react": "^18.2.0",
|
|
86
|
-
"semver": "^7.6.0",
|
|
87
88
|
"typescript": "^5.7.2",
|
|
88
89
|
"zod": "^3.23.8"
|
|
89
90
|
},
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DSH codec-contract compatibility check.
|
|
3
|
+
*
|
|
4
|
+
* The plugin's Typert Remote contribution must satisfy whichever strict-codec
|
|
5
|
+
* contract the installed DSH expects. DSH 0.1.6-alpha.2 replaced the
|
|
6
|
+
* materialized `schema` field with a lazy `create: () => TypertSchema` factory
|
|
7
|
+
* and rejects the old shape at plugin load, which took the whole contribution
|
|
8
|
+
* down at boot — a failure the unit tests could not see, because they never
|
|
9
|
+
* load the descriptors through a real registry.
|
|
10
|
+
*
|
|
11
|
+
* npm semver cannot wildcard prereleases, so the peer range enumerates them and
|
|
12
|
+
* a new DSH alpha silently falls outside it; pnpm only warns at install time.
|
|
13
|
+
* This check runs against a real DSH checkout, where the contract actually
|
|
14
|
+
* lives, and fails loudly when the two drift apart again.
|
|
15
|
+
*
|
|
16
|
+
* Run: `node scripts/verify-dsh-compat.mjs [path-to-dsh-checkout]`
|
|
17
|
+
* @module dsh-task-engine/scripts/verify-dsh-compat
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
21
|
+
import { join } from 'node:path'
|
|
22
|
+
import { fileURLToPath } from 'node:url'
|
|
23
|
+
|
|
24
|
+
const DEFAULT_DSH_ROOT = 'D:/dsharness/deepseek-harness'
|
|
25
|
+
|
|
26
|
+
const problems = []
|
|
27
|
+
function check(name, ok, detail = '') {
|
|
28
|
+
console.log(`${ok ? 'ok ' : 'FAIL'} ${name}${detail === '' ? '' : `: ${detail}`}`)
|
|
29
|
+
if (!ok) problems.push(name)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Locate the typert protocol package inside a DSH checkout. */
|
|
33
|
+
function protocolDir(root) {
|
|
34
|
+
const candidates = [
|
|
35
|
+
join(root, 'packages', 'typert', 'protocol'),
|
|
36
|
+
join(root, 'node_modules', '@deepseek-ai', 'dsh-typert-protocol'),
|
|
37
|
+
]
|
|
38
|
+
return candidates.find(dir => existsSync(join(dir, 'package.json')))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const dshRoot = process.argv[2] ?? DEFAULT_DSH_ROOT
|
|
42
|
+
console.log(`DSH checkout: ${dshRoot}`)
|
|
43
|
+
|
|
44
|
+
if (!existsSync(dshRoot)) {
|
|
45
|
+
// Absent checkout is not a failure: this script is advisory outside CI.
|
|
46
|
+
console.log('ok (skipped) no DSH checkout at that path')
|
|
47
|
+
process.exit(0)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const dir = protocolDir(dshRoot)
|
|
51
|
+
if (dir === undefined) {
|
|
52
|
+
check('locate the typert protocol package', false, 'not found under the DSH checkout')
|
|
53
|
+
process.exit(1)
|
|
54
|
+
}
|
|
55
|
+
const protocol = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'))
|
|
56
|
+
console.log(` typert protocol version: ${protocol.version}`)
|
|
57
|
+
|
|
58
|
+
// 1. Which contract does this DSH expect? Read the type, not a guess.
|
|
59
|
+
const typesPath = [
|
|
60
|
+
join(dir, 'src', 'types.ts'),
|
|
61
|
+
join(dir, 'lib', 'types', 'types.d.ts'),
|
|
62
|
+
].find(path => existsSync(path))
|
|
63
|
+
const typesText = typesPath === undefined ? '' : readFileSync(typesPath, 'utf8')
|
|
64
|
+
const wantsCreate = /readonly create: \(\) => TypertSchema/u.test(typesText)
|
|
65
|
+
const wantsSchema = /readonly schema: TypertSchema/u.test(typesText)
|
|
66
|
+
console.log(` contract: ${wantsCreate ? 'create() factory' : wantsSchema ? 'schema field' : 'unknown'}`)
|
|
67
|
+
|
|
68
|
+
// 2. Our descriptors must satisfy it. Import the built contribution and inspect
|
|
69
|
+
// every strict codec exactly as the DSH registry would.
|
|
70
|
+
const pluginRoot = fileURLToPath(new URL('..', import.meta.url))
|
|
71
|
+
const clientBundle = join(pluginRoot, 'lib', 'client.js')
|
|
72
|
+
if (!existsSync(clientBundle)) {
|
|
73
|
+
check('built client bundle exists', false, 'run `npm run build` first')
|
|
74
|
+
process.exit(1)
|
|
75
|
+
}
|
|
76
|
+
const bundleText = readFileSync(clientBundle, 'utf8')
|
|
77
|
+
const strictCodecs = (bundleText.match(/mode:\s*"strict"/gu) ?? []).length
|
|
78
|
+
const withCreate = (bundleText.match(/create:\s*\(\)\s*=>/gu) ?? []).length
|
|
79
|
+
const withSchema = (bundleText.match(/schema:/gu) ?? []).length
|
|
80
|
+
|
|
81
|
+
check('the bundle carries strict codecs', strictCodecs > 0, `${strictCodecs} found`)
|
|
82
|
+
if (wantsCreate) {
|
|
83
|
+
check('every strict codec provides create()', withCreate >= strictCodecs,
|
|
84
|
+
`${withCreate} of ${strictCodecs}`)
|
|
85
|
+
}
|
|
86
|
+
if (wantsSchema) {
|
|
87
|
+
check('every strict codec provides schema', withSchema >= strictCodecs,
|
|
88
|
+
`${withSchema} of ${strictCodecs}`)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 3. The peer range must actually admit the installed DSH version, because npm
|
|
92
|
+
// semver will not match a prerelease the range does not name. Implemented
|
|
93
|
+
// here rather than via the `semver` package: that is a devDependency, so a
|
|
94
|
+
// consumer running this script from the installed tarball has no copy, and
|
|
95
|
+
// the whole point of this check is to run against whatever DSH is present.
|
|
96
|
+
const pkg = JSON.parse(readFileSync(join(pluginRoot, 'package.json'), 'utf8'))
|
|
97
|
+
const range = pkg.peerDependencies['@deepseek-ai/dsh-typert-protocol'] ?? ''
|
|
98
|
+
check(`peer range admits DSH ${protocol.version}`, rangeAdmits(range, protocol.version), range)
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Whether one of a `||`-joined range's comparators admits `version`.
|
|
102
|
+
*
|
|
103
|
+
* npm semver matches a prerelease only when the comparator names a prerelease
|
|
104
|
+
* on the SAME major.minor.patch, which is why enumerating versions is the only
|
|
105
|
+
* way to cover a DSH alpha. This mirrors that rule for the caret and exact
|
|
106
|
+
* forms the manifest actually uses; it is deliberately conservative, and an
|
|
107
|
+
* unrecognized comparator reports `false` so the check fails loudly rather than
|
|
108
|
+
* passing by accident.
|
|
109
|
+
* @param range - the peer range expression.
|
|
110
|
+
* @param version - the installed DSH version to test.
|
|
111
|
+
* @returns true when the range admits the version.
|
|
112
|
+
*/
|
|
113
|
+
function rangeAdmits(range, version) {
|
|
114
|
+
const parse = text => {
|
|
115
|
+
const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/u.exec(text.trim())
|
|
116
|
+
return match === null
|
|
117
|
+
? undefined
|
|
118
|
+
: { major: +match[1], minor: +match[2], patch: +match[3], pre: match[4] }
|
|
119
|
+
}
|
|
120
|
+
const target = parse(version)
|
|
121
|
+
if (target === undefined) return false
|
|
122
|
+
for (const part of range.split('||').map(text => text.trim()).filter(Boolean)) {
|
|
123
|
+
const caret = /^\^(.+)$/u.exec(part)
|
|
124
|
+
const lower = parse(caret === null ? part : caret[1])
|
|
125
|
+
if (lower === undefined) continue
|
|
126
|
+
// Same prerelease tuple, or a plain release on the same caret line.
|
|
127
|
+
const sameTuple = target.major === lower.major
|
|
128
|
+
&& target.minor === lower.minor
|
|
129
|
+
&& target.patch === lower.patch
|
|
130
|
+
if (lower.pre !== undefined) {
|
|
131
|
+
if (sameTuple) return true
|
|
132
|
+
} else if (target.pre === undefined && target.major === lower.major) {
|
|
133
|
+
if (caret === null ? sameTuple : target.minor >= lower.minor) return true
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return false
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (problems.length > 0) {
|
|
140
|
+
console.error(`\n${problems.length} DSH compatibility problem(s)`)
|
|
141
|
+
console.error('A new DSH alpha likely changed the Typert contract. Update src/client/remote.ts')
|
|
142
|
+
console.error('and add the version to peerDependencies before releasing.')
|
|
143
|
+
process.exit(1)
|
|
144
|
+
}
|
|
145
|
+
console.log('\nDSH compatibility verified')
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Black-box verification of the npm tarball before publishing: packs the
|
|
3
|
+
* package, extracts it into a clean temp dir, and proves the published entry
|
|
4
|
+
* points actually work �?main import, browser client registration, the bundled
|
|
5
|
+
* `.p0-test.mjs`, the CLI, and the `files` whitelist.
|
|
6
|
+
*
|
|
7
|
+
* Run: `npm run verify:package`
|
|
8
|
+
* @module dsh-task-engine/scripts/verify-package
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { execSync } from 'node:child_process'
|
|
12
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs'
|
|
13
|
+
import { tmpdir } from 'node:os'
|
|
14
|
+
import { join } from 'node:path'
|
|
15
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
16
|
+
|
|
17
|
+
const pkgRoot = fileURLToPath(new URL('..', import.meta.url))
|
|
18
|
+
const failures = []
|
|
19
|
+
function check(name, ok, detail = '') {
|
|
20
|
+
console.log(`${ok ? 'ok ' : 'FAIL'} ${name}${detail !== '' ? `: ${detail}` : ''}`)
|
|
21
|
+
if (!ok) failures.push(name)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// 1. pack
|
|
25
|
+
let tgzName
|
|
26
|
+
try {
|
|
27
|
+
const packed = JSON.parse(execSync('npm pack --json', { cwd: pkgRoot, encoding: 'utf8' }))
|
|
28
|
+
tgzName = packed[0]?.filename
|
|
29
|
+
} catch (error) {
|
|
30
|
+
check('npm pack', false, String(error))
|
|
31
|
+
process.exit(1)
|
|
32
|
+
}
|
|
33
|
+
check('npm pack', typeof tgzName === 'string', tgzName)
|
|
34
|
+
|
|
35
|
+
// 2. extract into a clean temp dir
|
|
36
|
+
const tmp = mkdtempSync(join(tmpdir(), 'dsh-pkg-verify-'))
|
|
37
|
+
const tgzPath = join(pkgRoot, tgzName)
|
|
38
|
+
execSync(`tar -xzf "${tgzPath}"`, { cwd: tmp, stdio: 'pipe' })
|
|
39
|
+
const pkgDir = join(tmp, 'package')
|
|
40
|
+
const listing = execSync(`tar -tzf "${tgzPath}"`, { encoding: 'utf8' })
|
|
41
|
+
.trim().split(/\r?\n/u).map(line => line.replace(/^package\//u, ''))
|
|
42
|
+
check('extract', existsSync(join(pkgDir, 'package.json')))
|
|
43
|
+
|
|
44
|
+
// 3. files whitelist sanity
|
|
45
|
+
for (const required of ['hooks/commit-msg', 'lib/index.js', 'lib/client.js', 'lib/client.d.ts', '.p0-test.mjs', 'preset/enable.mjs', 'README.md', '.resource-test.mjs', '.workflow-test.mjs', '.hook-test.mjs', '.preset-test.mjs']) {
|
|
46
|
+
check(`tarball contains ${required}`, listing.includes(required))
|
|
47
|
+
}
|
|
48
|
+
check('tarball excludes src sources', !listing.some(line => line.startsWith('src/')))
|
|
49
|
+
check('tarball excludes build scripts', !listing.some(line => line.endsWith('build-client.mjs') || line.endsWith('build-hook.mjs')))
|
|
50
|
+
|
|
51
|
+
// Every `node <file>` script the manifest declares must actually ship, EXCEPT
|
|
52
|
+
// the build-time ones that `prepare` needs in the source tree only. A script
|
|
53
|
+
// whose target is absent from `files` installs fine and then dies with
|
|
54
|
+
// MODULE_NOT_FOUND �?which is exactly what verify:package and verify:dsh did
|
|
55
|
+
// until this assertion existed, because neither scripts/ path was published.
|
|
56
|
+
{
|
|
57
|
+
const manifest = JSON.parse(readFileSync(join(pkgRoot, 'package.json'), 'utf8'))
|
|
58
|
+
// Scripts that legitimately run only before publishing, never from a consumer.
|
|
59
|
+
const BUILD_ONLY = new Set(['prepare', 'build'])
|
|
60
|
+
const missing = []
|
|
61
|
+
for (const [name, command] of Object.entries(manifest.scripts ?? {})) {
|
|
62
|
+
if (BUILD_ONLY.has(name)) continue
|
|
63
|
+
for (const match of String(command).matchAll(/\bnode\s+(?:--[^\s]+\s+)*([^\s&|]+)/gu)) {
|
|
64
|
+
const target = match[1].replace(/^\.\//u, '')
|
|
65
|
+
if (!target.endsWith('.mjs') && !target.endsWith('.js')) continue
|
|
66
|
+
if (!listing.includes(target)) missing.push(`${name} -> ${target}`)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
check('every declared node script ships in the tarball', missing.length === 0, missing.join(', '))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 3b. install the tarball into a clean project so peer dependencies resolve,
|
|
73
|
+
// exactly like a real consumer's `npm install`.
|
|
74
|
+
const installDir = mkdtempSync(join(tmpdir(), 'dsh-pkg-install-'))
|
|
75
|
+
import { writeFileSync } from 'node:fs'
|
|
76
|
+
writeFileSync(join(installDir, 'package.json'), JSON.stringify({ name: 'probe', private: true, type: 'module' }))
|
|
77
|
+
try {
|
|
78
|
+
execSync(`npm install "${tgzPath}" --no-audit --no-fund`, { cwd: installDir, encoding: 'utf8', stdio: 'pipe' })
|
|
79
|
+
check('npm install tarball', true)
|
|
80
|
+
} catch (error) {
|
|
81
|
+
check('npm install tarball', false, String(error.stderr ?? error))
|
|
82
|
+
process.exit(1)
|
|
83
|
+
}
|
|
84
|
+
const installedDir = join(installDir, 'node_modules', '@godv61', 'dsh-task-engine')
|
|
85
|
+
|
|
86
|
+
// 4. main entry imports
|
|
87
|
+
try {
|
|
88
|
+
await import(pathToFileURL(join(installedDir, 'lib/index.js')).href)
|
|
89
|
+
check('import main entry', true)
|
|
90
|
+
} catch (error) {
|
|
91
|
+
check('import main entry', false, error instanceof Error ? error.message : String(error))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 5. browser client registers through the DSH module loader
|
|
95
|
+
try {
|
|
96
|
+
const registry = { loaded: undefined }
|
|
97
|
+
globalThis.window = { __ModuleLoader__: { load(reg) { registry.loaded = reg } } }
|
|
98
|
+
await import(pathToFileURL(join(installedDir, 'lib/client.js')).href)
|
|
99
|
+
check('client bundle registers via ModuleLoader', registry.loaded?.id === '@godv61/dsh-task-engine')
|
|
100
|
+
} catch (error) {
|
|
101
|
+
check('client bundle registers via ModuleLoader', false, error instanceof Error ? error.message : String(error))
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 6. in-package acceptance test
|
|
105
|
+
try {
|
|
106
|
+
const out = execSync('node .p0-test.mjs', { cwd: installedDir, encoding: 'utf8', stdio: 'pipe' })
|
|
107
|
+
check('in-package .p0-test.mjs', /passed/u.test(out), out.trim().split('\n').at(-1))
|
|
108
|
+
} catch (error) {
|
|
109
|
+
check('in-package .p0-test.mjs', false, String(error.stderr ?? error))
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Resource package behavior from the installed artifact, including the commit
|
|
113
|
+
// hook running for real inside throwaway repositories.
|
|
114
|
+
try {
|
|
115
|
+
const out = execSync('node --test --test-reporter=tap .resource-test.mjs .workflow-test.mjs .hook-test.mjs .preset-test.mjs', { cwd: installedDir, encoding: 'utf8', stdio: 'pipe' })
|
|
116
|
+
check('in-package resource, workflow and hook behavior', /# fail 0/u.test(out))
|
|
117
|
+
} catch (error) { check('in-package resource, workflow and hook behavior', false, String(error.stderr ?? error)) }
|
|
118
|
+
|
|
119
|
+
// 7. CLI parses
|
|
120
|
+
try {
|
|
121
|
+
const cli = join(installedDir, 'preset', 'enable.mjs')
|
|
122
|
+
execSync(`node --check "${cli}"`, { stdio: 'pipe' })
|
|
123
|
+
check('CLI enable.mjs parses', true)
|
|
124
|
+
} catch (error) {
|
|
125
|
+
check('CLI enable.mjs parses', false, String(error.stderr ?? error))
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
rmSync(tmp, { recursive: true, force: true })
|
|
129
|
+
rmSync(installDir, { recursive: true, force: true })
|
|
130
|
+
try { rmSync(tgzPath, { force: true }) } catch { /* keep the tarball when the FS declines */ }
|
|
131
|
+
|
|
132
|
+
if (failures.length > 0) {
|
|
133
|
+
console.error(`\n${failures.length} failure(s)`)
|
|
134
|
+
process.exit(1)
|
|
135
|
+
}
|
|
136
|
+
console.log('\npackage black-box verification passed')
|