create-harness-vibe-coding 0.8.10 → 0.8.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README-CN.md +16 -4
- package/README.md +17 -5
- package/docs/images/harness-architecture.drawio +2 -2
- package/package.json +3 -2
- package/src/index.js +8 -0
- package/templates/common/.claude/commands/wf-update.md +7 -1
- package/templates/common/.claude/skills/wf-update/SKILL.md +11 -1
- package/templates/common/.harness-version +13 -13
- package/templates/common/.opencode/commands/wf-update.md +7 -1
- package/templates/common/Harness/SETUP.md +1 -1
- package/templates/common/Harness/WF-AUTO-SPARK.md +21 -4
- package/templates/common/Harness/WF-MAX.md +36 -2
- package/templates/common/Harness/scripts/scan-clean.mjs +77 -5
- package/templates/common/Harness/scripts/validate-harness.mjs +24 -0
- package/templates/common/Harness/scripts/wf-update-check.mjs +223 -92
package/README-CN.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<img src="https://img.shields.io/npm/v/create-harness-vibe-coding?color=blue" alt="npm version">
|
|
3
3
|
<img src="https://img.shields.io/badge/node-%3E%3D18-brightgreen" alt="Node.js >= 18">
|
|
4
4
|
<img src="https://img.shields.io/npm/l/create-harness-vibe-coding" alt="MIT license">
|
|
5
|
-
<img src="https://img.shields.io/github/stars/
|
|
5
|
+
<img src="https://img.shields.io/github/stars/LiWeny16/create-harness-vibe-coding?style=social" alt="GitHub stars">
|
|
6
6
|
</p>
|
|
7
7
|
|
|
8
8
|
<p align="center">
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
<p align="center">
|
|
19
19
|
<a href="README.md">English</a> ·
|
|
20
|
-
<a href="https://github.com/
|
|
20
|
+
<a href="https://github.com/LiWeny16/create-harness-vibe-coding">GitHub</a> ·
|
|
21
21
|
<a href="https://www.npmjs.com/package/create-harness-vibe-coding">npm</a>
|
|
22
22
|
</p>
|
|
23
23
|
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
|
|
34
34
|
把下面这句话原样丢给正在使用的 Agent:
|
|
35
35
|
|
|
36
|
-
> 请根据 https://github.com/
|
|
36
|
+
> 请根据 https://github.com/LiWeny16/create-harness-vibe-coding/blob/main/README-CN.md 安装 Harness 框架。
|
|
37
37
|
|
|
38
38
|
## 安装说明(Agent instruction)
|
|
39
39
|
|
|
@@ -191,8 +191,20 @@ node Harness/scripts/validate-harness.mjs
|
|
|
191
191
|
|
|
192
192
|
# bootstrap 完成后或发布前
|
|
193
193
|
node Harness/scripts/validate-harness.mjs --strict
|
|
194
|
+
|
|
195
|
+
# 发布更新版本后
|
|
196
|
+
npm run check:mirrors
|
|
194
197
|
```
|
|
195
198
|
|
|
199
|
+
## 发布门禁
|
|
200
|
+
|
|
201
|
+
铁律:每次 Harness 更新发版都必须同时保持两个更新通道可用:
|
|
202
|
+
|
|
203
|
+
- Canonical:npm `create-harness-vibe-coding@latest` 和 `https://github.com/LiWeny16/create-harness-vibe-coding`
|
|
204
|
+
- 低版本兼容镜像:`https://github.com/zingspark/create-harness-vibe-coding`
|
|
205
|
+
|
|
206
|
+
低版本用户的 updater 可能已经硬编码到旧的 `zingspark` 仓库。发布不能只更新新仓库;必须等 legacy mirror 拥有同一份 commit、tag 和生成后的 template manifest,才算发版完成。新安装生成的 `Harness/.harness-version` 仍然记录 canonical `LiWeny16` source;`zingspark` 只作为旧用户兼容镜像保留。
|
|
207
|
+
|
|
196
208
|
## 适配范围与体积
|
|
197
209
|
|
|
198
210
|
- 支持 Claude Code、Codex 和 OpenCode 的共享 Harness 工作流。
|
|
@@ -219,4 +231,4 @@ my-project/
|
|
|
219
231
|
└── .opencode/ ← OpenCode 配置
|
|
220
232
|
```
|
|
221
233
|
|
|
222
|
-
MIT © [
|
|
234
|
+
MIT © [LiWeny16](https://github.com/LiWeny16)
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<img src="https://img.shields.io/npm/v/create-harness-vibe-coding?color=blue" alt="npm version">
|
|
3
3
|
<img src="https://img.shields.io/badge/node-%3E%3D18-brightgreen" alt="Node.js >= 18">
|
|
4
4
|
<img src="https://img.shields.io/npm/l/create-harness-vibe-coding" alt="MIT license">
|
|
5
|
-
<img src="https://img.shields.io/github/stars/
|
|
5
|
+
<img src="https://img.shields.io/github/stars/LiWeny16/create-harness-vibe-coding?style=social" alt="GitHub stars">
|
|
6
6
|
</p>
|
|
7
7
|
|
|
8
8
|
<p align="center">
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
<p align="center">
|
|
19
19
|
<a href="README-CN.md">Chinese</a> ·
|
|
20
|
-
<a href="https://github.com/
|
|
20
|
+
<a href="https://github.com/LiWeny16/create-harness-vibe-coding">GitHub</a> ·
|
|
21
21
|
<a href="https://www.npmjs.com/package/create-harness-vibe-coding">npm</a>
|
|
22
22
|
</p>
|
|
23
23
|
|
|
@@ -44,7 +44,7 @@ Idea → Research → PRD → Architecture → Acceptance Criteria
|
|
|
44
44
|
|
|
45
45
|
Paste this into your agent:
|
|
46
46
|
|
|
47
|
-
> Please install the Harness framework by following https://github.com/
|
|
47
|
+
> Please install the Harness framework by following https://github.com/LiWeny16/create-harness-vibe-coding/blob/main/README.md.
|
|
48
48
|
|
|
49
49
|
## Installation instructions (Agent instruction)
|
|
50
50
|
|
|
@@ -147,7 +147,7 @@ The generated project starts with no business stack or generated application cod
|
|
|
147
147
|
Paste this to your agent:
|
|
148
148
|
|
|
149
149
|
```text
|
|
150
|
-
Read and follow https://github.com/
|
|
150
|
+
Read and follow https://github.com/LiWeny16/create-harness-vibe-coding exactly to configure this project with create-harness-vibe-coding.
|
|
151
151
|
```
|
|
152
152
|
|
|
153
153
|
The agent-first path previews the target before writing and preserves project-owned files. If you need the detailed CLI contract, the agent can read `Harness/SETUP.md` after setup.
|
|
@@ -189,8 +189,20 @@ node Harness/scripts/validate-harness.mjs
|
|
|
189
189
|
|
|
190
190
|
# Generated project after bootstrap or before release
|
|
191
191
|
node Harness/scripts/validate-harness.mjs --strict
|
|
192
|
+
|
|
193
|
+
# After publishing an update release
|
|
194
|
+
npm run check:mirrors
|
|
192
195
|
```
|
|
193
196
|
|
|
197
|
+
## Release gate
|
|
198
|
+
|
|
199
|
+
Iron rule: every update release must keep both update channels live:
|
|
200
|
+
|
|
201
|
+
- Canonical: npm `create-harness-vibe-coding@latest` and `https://github.com/LiWeny16/create-harness-vibe-coding`
|
|
202
|
+
- Legacy compatibility mirror: `https://github.com/zingspark/create-harness-vibe-coding`
|
|
203
|
+
|
|
204
|
+
Low-version installs can have updater scripts hardcoded to the legacy mirror. Do not mark a release complete until the legacy mirror exposes the same commit, tag, and generated template manifest as canonical. Generated installs still record the canonical `LiWeny16` source; the `zingspark` repo is kept for backward compatibility.
|
|
205
|
+
|
|
194
206
|
## Footprint
|
|
195
207
|
|
|
196
208
|
| | |
|
|
@@ -204,4 +216,4 @@ node Harness/scripts/validate-harness.mjs --strict
|
|
|
204
216
|
|
|
205
217
|
AI coding agent framework · agentic workflow · context engineering · long-running coding tasks · task orchestration · durable memory · safe merge · conflict handling · acceptance criteria · Claude Code · Codex · OpenCode · Node.js CLI · developer productivity
|
|
206
218
|
|
|
207
|
-
MIT © [
|
|
219
|
+
MIT © [LiWeny16](https://github.com/LiWeny16)
|
|
@@ -106,7 +106,7 @@
|
|
|
106
106
|
<mxCell id="80" value="One command. Zero drift. Agent success, life success." style="text;html=1;align=center;verticalAlign=middle;fontSize=12;fontStyle=0;fontColor=#374151;" vertex="1" parent="1" >
|
|
107
107
|
<mxGeometry x="350" y="880" width="600" height="24" as="geometry" />
|
|
108
108
|
</mxCell>
|
|
109
|
-
<mxCell id="81" value="MIT ©
|
|
109
|
+
<mxCell id="81" value="MIT © LiWeny16 · github.com/LiWeny16/create-harness-vibe-coding" style="text;html=1;align=center;verticalAlign=middle;fontSize=10;fontStyle=0;fontColor=#9ca3af;" vertex="1" parent="1">
|
|
110
110
|
<mxGeometry x="400" y="910" width="500" height="18" as="geometry" />
|
|
111
111
|
</mxCell>
|
|
112
112
|
<mxCell id="82" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#4f46e5;strokeColor=none;opacity=30;" vertex="1" parent="1" >
|
|
@@ -161,4 +161,4 @@
|
|
|
161
161
|
</root>
|
|
162
162
|
</mxGraphModel>
|
|
163
163
|
</diagram>
|
|
164
|
-
</mxfile>
|
|
164
|
+
</mxfile>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-harness-vibe-coding",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.13",
|
|
4
4
|
"description": "Scaffold a 0-1 product harness for AI-assisted research, PRD, planning, architecture, build, test, and feedback loops",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"scripts": {
|
|
19
19
|
"start": "node src/index.js",
|
|
20
20
|
"build:version": "node scripts/build-version.mjs",
|
|
21
|
+
"check:mirrors": "node scripts/check-update-mirrors.mjs",
|
|
21
22
|
"prepush": "node scripts/pre-push-check.mjs",
|
|
22
23
|
"test": "node --test tests/*.test.js",
|
|
23
24
|
"test:e2e": "node tests/e2e-wf-scripts.test.mjs",
|
|
@@ -33,7 +34,7 @@
|
|
|
33
34
|
},
|
|
34
35
|
"repository": {
|
|
35
36
|
"type": "git",
|
|
36
|
-
"url": "git+https://github.com/
|
|
37
|
+
"url": "git+https://github.com/LiWeny16/create-harness-vibe-coding.git"
|
|
37
38
|
},
|
|
38
39
|
"keywords": [
|
|
39
40
|
"claude-code",
|
package/src/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import { generate, getOptionalCatalog } from './generator.js';
|
|
|
9
9
|
|
|
10
10
|
const UPDATE_SUCCESS_STATUSES = new Set(['up-to-date', 'update-available', 'partial-update']);
|
|
11
11
|
const UPDATE_FAILURE_STATUSES = new Set(['error', 'offline', 'template-remote', 'downgrade-refused']);
|
|
12
|
+
const CANONICAL_UPDATE_SOURCE_BASE = 'https://raw.githubusercontent.com/LiWeny16/create-harness-vibe-coding/main/templates/common/';
|
|
12
13
|
|
|
13
14
|
// ── CLI flags ──────────────────────────────────────────────
|
|
14
15
|
const raw = process.argv.slice(2);
|
|
@@ -515,6 +516,7 @@ function runUpdateSwitch(scan, { json }) {
|
|
|
515
516
|
console.log(pc.yellow('Existing Harness detected. Switching to wf-update check.'));
|
|
516
517
|
console.log(pc.dim(`Directory ${scan.resolvedDir}`));
|
|
517
518
|
console.log(pc.dim('Command node Harness/scripts/wf-update-check.mjs'));
|
|
519
|
+
console.log(pc.dim(`Source ${CANONICAL_UPDATE_SOURCE_BASE}`));
|
|
518
520
|
console.log('');
|
|
519
521
|
|
|
520
522
|
if (!updateResult.success && updateResult.error) {
|
|
@@ -555,12 +557,14 @@ function createUpdateSwitchResult(scan, { json }) {
|
|
|
555
557
|
updateCommand: json
|
|
556
558
|
? 'node Harness/scripts/wf-update-check.mjs --json'
|
|
557
559
|
: 'node Harness/scripts/wf-update-check.mjs',
|
|
560
|
+
updateSourceBase: CANONICAL_UPDATE_SOURCE_BASE,
|
|
558
561
|
next: [
|
|
559
562
|
{
|
|
560
563
|
action: 'update',
|
|
561
564
|
command: json
|
|
562
565
|
? 'node Harness/scripts/wf-update-check.mjs --json'
|
|
563
566
|
: 'node Harness/scripts/wf-update-check.mjs',
|
|
567
|
+
env: { WF_SOURCE_BASE: CANONICAL_UPDATE_SOURCE_BASE },
|
|
564
568
|
reason: 'Harness already exists, so updates must use the installed Harness update flow.',
|
|
565
569
|
},
|
|
566
570
|
],
|
|
@@ -589,6 +593,10 @@ function createUpdateSwitchResult(scan, { json }) {
|
|
|
589
593
|
const result = spawnSync(process.execPath, args, {
|
|
590
594
|
cwd: scan.resolvedDir,
|
|
591
595
|
encoding: 'utf8',
|
|
596
|
+
env: {
|
|
597
|
+
...process.env,
|
|
598
|
+
WF_SOURCE_BASE: CANONICAL_UPDATE_SOURCE_BASE,
|
|
599
|
+
},
|
|
592
600
|
});
|
|
593
601
|
const stdout = result.stdout || '';
|
|
594
602
|
const stderr = result.stderr || '';
|
|
@@ -4,7 +4,7 @@ Run the Harness update checker script. Do not invoke a skill or start WF mode.
|
|
|
4
4
|
|
|
5
5
|
## Flow
|
|
6
6
|
|
|
7
|
-
1. Run `node Harness/scripts/wf-update-check.mjs --json` and use the `agent` block as the action plan.
|
|
7
|
+
1. Run `node Harness/scripts/wf-update-check.mjs --json` and use the `agent` block as the action plan. Current updaters try npm `create-harness-vibe-coding@latest` first, then the canonical GitHub source `LiWeny16/create-harness-vibe-coding`, then the legacy compatibility mirror `zingspark/create-harness-vibe-coding`.
|
|
8
8
|
2. Preserve all PRESERVE files. Never overwrite user task, memory, research, README, package, or architecture files.
|
|
9
9
|
3. If `agent.safeApplyCommand` is present, run it to apply SAFE/NEW files first.
|
|
10
10
|
4. For conflicts, compare local with `templateHint` or `remoteUrl`, decide merge/keep-local/overwrite, and record via `--accept-local`, `--accept-merged`, or `--accept-template`.
|
|
@@ -22,3 +22,9 @@ npx create-harness-vibe-coding@latest <project-name> . -y --on-conflict skip
|
|
|
22
22
|
```
|
|
23
23
|
|
|
24
24
|
This creates missing Harness files while preserving CLAUDE.md, README.md, tasks, memory, research, and all user data. After recovery, re-run the update check.
|
|
25
|
+
|
|
26
|
+
If an old updater reports only `0.8.10`, run the latest installer command above or re-run the checker with:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
node Harness/scripts/wf-update-check.mjs --json --source-base https://raw.githubusercontent.com/LiWeny16/create-harness-vibe-coding/main/templates/common/
|
|
30
|
+
```
|
|
@@ -23,7 +23,10 @@ This skill is a Codex compatibility shim plus script-flow reference. Claude Code
|
|
|
23
23
|
## Flow
|
|
24
24
|
|
|
25
25
|
1. Run `node Harness/scripts/wf-update-check.mjs --json` first and use the
|
|
26
|
-
`agent` block as the action plan.
|
|
26
|
+
`agent` block as the action plan. Current updaters try npm
|
|
27
|
+
`create-harness-vibe-coding@latest` first, then the canonical GitHub source
|
|
28
|
+
`LiWeny16/create-harness-vibe-coding`, then the legacy compatibility mirror
|
|
29
|
+
`zingspark/create-harness-vibe-coding`.
|
|
27
30
|
2. Preserve all PRESERVE files. Never overwrite user task, memory, research,
|
|
28
31
|
README, package, or architecture files.
|
|
29
32
|
3. If `agent.safeApplyCommand` is present, run it to apply SAFE, NEW, and
|
|
@@ -50,6 +53,13 @@ npx create-harness-vibe-coding@latest <project-name> . -y --on-conflict skip
|
|
|
50
53
|
|
|
51
54
|
The `--on-conflict skip` policy preserves all existing user files (CLAUDE.md, README.md, tasks, memory, research, architecture) and only creates missing Harness infrastructure files. After recovery, re-run the update check.
|
|
52
55
|
|
|
56
|
+
If an old updater reports only `0.8.10`, run the latest installer command above
|
|
57
|
+
or re-run the checker with:
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
node Harness/scripts/wf-update-check.mjs --json --source-base https://raw.githubusercontent.com/LiWeny16/create-harness-vibe-coding/main/templates/common/
|
|
61
|
+
```
|
|
62
|
+
|
|
53
63
|
## Return
|
|
54
64
|
|
|
55
65
|
Report version, SAFE/NEW updates, conflicts and decisions, preserved files,
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"generator": "0.8.
|
|
3
|
-
"generated": "2026-07-
|
|
2
|
+
"generator": "0.8.13",
|
|
3
|
+
"generated": "2026-07-22T09:32:47.044Z",
|
|
4
4
|
"options": [],
|
|
5
5
|
"autoCheck": true,
|
|
6
|
-
"source": "https://raw.githubusercontent.com/
|
|
6
|
+
"source": "https://raw.githubusercontent.com/LiWeny16/create-harness-vibe-coding/main/templates/common/",
|
|
7
7
|
"checksums": {
|
|
8
8
|
".agents/skills/subagent-orchestrator/SKILL.md": "sha256-8d9e8c0db4647bcf168a5f99c8b2ea97d154bfca30e78ffd1cc5a98ce185bb1d",
|
|
9
9
|
".agents/skills/tdd/SKILL.md": "sha256-1e2ec0b54a5cf5b071800b0f2e5672a128a42471e16f70f00bb4c0a07a3617cb",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
".agents/skills/wf-readme/SKILL.md": "sha256-4d34ee3b6b305fa2581f5d8cb31e9152759f3df8bf8fa08efbb43782f128bdcd",
|
|
15
15
|
".agents/skills/wf-remove/SKILL.md": "sha256-21ebef1a1fa8645d094c8ceeca78b33f036b6c42cda6db8be425d38d9e91af75",
|
|
16
16
|
".agents/skills/wf-review/SKILL.md": "sha256-ab1122cbc9612ba734f98736e5479d7777206293d011d5471aaa34d1da35dae6",
|
|
17
|
-
".agents/skills/wf-update/SKILL.md": "sha256-
|
|
17
|
+
".agents/skills/wf-update/SKILL.md": "sha256-a0942270455063911bfc603fe6e5fad3e0247e845fc8095e5b4a54fc883e1fba",
|
|
18
18
|
".agents/skills/wf/SKILL.md": "sha256-53225645b593f4cf060781590f9b3025cf17315baad4e2acce850cc4cd972f97",
|
|
19
19
|
".claude/agents/architect-manager.md": "sha256-d98c3a2d044d5216d9bcde217b4359b1c33f5be152416e242807652ddcd2868b",
|
|
20
20
|
".claude/agents/architect.md": "sha256-61cea40c8c3e01bd53f3ad642ef734850fdb931497e7ee20552ea7a8b4e7b70c",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
".claude/agents/test-writer.md": "sha256-724866d627f193caaf060ec6af97b1d54c2df2d5c5f937e78fc3c53c7f798b16",
|
|
37
37
|
".claude/agents/verifier.md": "sha256-d501a505b97e679daa89ee1d910ee185edc42ba57d58203e962b1c5a844c0872",
|
|
38
38
|
".claude/commands/wf-help.md": "sha256-aaaa6834c6cb61d2ecbeb41ad8fcf77de10ac1638b2009a3822284c01cf41fac",
|
|
39
|
-
".claude/commands/wf-update.md": "sha256-
|
|
39
|
+
".claude/commands/wf-update.md": "sha256-5a92a2b32952b6dac6b9ef318c63bc049d68c9dbf2623370a116c4fe602392dd",
|
|
40
40
|
".claude/rules/ecc/common.md": "sha256-e5f046d02ec9f5d326088f608c63758632d9a5f6c67688b20e065b6380884992",
|
|
41
41
|
".claude/settings.json": "sha256-99e8552584b8e71e3e11b4b760c3bfbf8c1b18dadda7ec3f6e3c46f88d9ea513",
|
|
42
42
|
".claude/skills/subagent-orchestrator/SKILL.md": "sha256-8d9e8c0db4647bcf168a5f99c8b2ea97d154bfca30e78ffd1cc5a98ce185bb1d",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
".claude/skills/wf-readme/SKILL.md": "sha256-4d34ee3b6b305fa2581f5d8cb31e9152759f3df8bf8fa08efbb43782f128bdcd",
|
|
49
49
|
".claude/skills/wf-remove/SKILL.md": "sha256-21ebef1a1fa8645d094c8ceeca78b33f036b6c42cda6db8be425d38d9e91af75",
|
|
50
50
|
".claude/skills/wf-review/SKILL.md": "sha256-ab1122cbc9612ba734f98736e5479d7777206293d011d5471aaa34d1da35dae6",
|
|
51
|
-
".claude/skills/wf-update/SKILL.md": "sha256-
|
|
51
|
+
".claude/skills/wf-update/SKILL.md": "sha256-a0942270455063911bfc603fe6e5fad3e0247e845fc8095e5b4a54fc883e1fba",
|
|
52
52
|
".claude/skills/wf/SKILL.md": "sha256-53225645b593f4cf060781590f9b3025cf17315baad4e2acce850cc4cd972f97",
|
|
53
53
|
".codex/config.toml": "sha256-e0af046982c6dbc535c52b19610d16ce9aba08a17dd29f11b74cc8dad8371145",
|
|
54
54
|
".codex/hooks.json": "sha256-3200c42c2da434c07cd6ab1ce86f8845ab46a1ed9a25b65117b2b2b42056c725",
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
".opencode/commands/wf-readme.md": "sha256-276e80255bd38f51e17b158650d182401304bf06388407d259f47850306556e8",
|
|
80
80
|
".opencode/commands/wf-remove.md": "sha256-fe9823152c4f74e66b151e352de7167436d1bc47376c2a688d190391931d300a",
|
|
81
81
|
".opencode/commands/wf-review.md": "sha256-4bd59f66bc96c2773868e63c37cbacf39dd521555e997c0b60d50538ef2d5dbd",
|
|
82
|
-
".opencode/commands/wf-update.md": "sha256-
|
|
82
|
+
".opencode/commands/wf-update.md": "sha256-5a92a2b32952b6dac6b9ef318c63bc049d68c9dbf2623370a116c4fe602392dd",
|
|
83
83
|
".opencode/commands/wf.md": "sha256-d86cf20a78c37757f76aed7e252475fb82e7597cd55c6a4c916441b3b75fe9af",
|
|
84
84
|
".opencode/plugins/harness-wf-status.mjs": "sha256-ee0987cec5530c34784c619a803562d7085ae26fe5d199fde7ce42c2bed4724e",
|
|
85
85
|
"AGENTS.md": "sha256-fad176931742a5080e073d1ddba99a3eabb67e8c42bebb132df99a51ca6a184d",
|
|
@@ -92,14 +92,14 @@
|
|
|
92
92
|
"Harness/MEMORY.md": "sha256-4cbb881b3e9893e917861dc250afaedf5b88da6c99626a9c7f6abb8132841b09",
|
|
93
93
|
"Harness/MEMORY_PROTOCOL.md": "sha256-c8cda68bf25fbf7152c6049959a8bd6d39453976a651a1c4a9d3f7e349f823c4",
|
|
94
94
|
"Harness/README.md": "sha256-4c8f30de9face43960088d240127860b43a2b821d1988c23a9cd1c0502ba35f7",
|
|
95
|
-
"Harness/SETUP.md": "sha256-
|
|
95
|
+
"Harness/SETUP.md": "sha256-3265cec5114ce42200d0555771e07d2355bcb5dfdf26c8725226e28df67eeaa4",
|
|
96
96
|
"Harness/TASK_ARCHIVE.md": "sha256-d815f2c99a7dc98b9956bb876e9ee07dfb66a283c758124d7b4eb8965dea1f6a",
|
|
97
97
|
"Harness/TDD-GUIDE.md": "sha256-602084530fd0dea7499a023ce75738351e5d97a0c222b64d405f36da6b59a162",
|
|
98
98
|
"Harness/WF-AUTO-ANGLES.md": "sha256-b9641d871f728e8112ad0d8c7f87bcbec0a06aa572c5f3060efdd26496abdf9b",
|
|
99
|
-
"Harness/WF-AUTO-SPARK.md": "sha256-
|
|
99
|
+
"Harness/WF-AUTO-SPARK.md": "sha256-bfd5096d4994d87bc79e72e460cd291403f6240054bbc6ce4b08503196d29c1f",
|
|
100
100
|
"Harness/WF-AUTO.md": "sha256-7898a38ee9ce9cf1c49aa7e3603c17afc68544981dca7d91e2e4149ca2fc5aea",
|
|
101
101
|
"Harness/WF-KERNEL.md": "sha256-97c626574f62dceaa590829d99e46889b86cab5658d7827c704788e4afdda030",
|
|
102
|
-
"Harness/WF-MAX.md": "sha256-
|
|
102
|
+
"Harness/WF-MAX.md": "sha256-92f7d23ebaf935142cc741833a7cb98c3cfab1aa845551b291319e0f8817d018",
|
|
103
103
|
"Harness/WF-STATE.md": "sha256-80fdaa4b133a9f632a252e4d2fca51bb96f9f9779dab2bc6f3d791d0d2d3aae8",
|
|
104
104
|
"Harness/WF.md": "sha256-199f6c3899cf0d8c86d65fd61e90e85ee9f8aa4c8fc56889f995f05e11987816",
|
|
105
105
|
"Harness/agent-workflow.md": "sha256-a91c3732ffb545506cec88c875d76ebe92248d0714da3e398d2a13b06349bef4",
|
|
@@ -109,11 +109,11 @@
|
|
|
109
109
|
"Harness/lifecycle.md": "sha256-a2d025e52f3b4412d8439083aca0d2cf7a4458ad0e30de4fc6465f990379190f",
|
|
110
110
|
"Harness/research/README.md": "sha256-956c154df01cb9376972eb8b88bb3be0fe4e2dda802a72457c0126e3476fa755",
|
|
111
111
|
"Harness/scripts/archive-tasks.mjs": "sha256-07bba20c2f4b3baae737b8b9a951125a48e37dd482a1f1ca122b9cf88e90e956",
|
|
112
|
-
"Harness/scripts/scan-clean.mjs": "sha256-
|
|
113
|
-
"Harness/scripts/validate-harness.mjs": "sha256-
|
|
112
|
+
"Harness/scripts/scan-clean.mjs": "sha256-2b414738317cfe41432926b72691edb55ce313ff2427fd1312c1947bf687da61",
|
|
113
|
+
"Harness/scripts/validate-harness.mjs": "sha256-a6174f918453943387df8015664f81599ea74a179bec351d22187ec0a19a1e7c",
|
|
114
114
|
"Harness/scripts/wf-auto-update-prompt.mjs": "sha256-14210304dbd8e648b8939fc4c5d3664f943052153e689264c16b05b7b81bd39e",
|
|
115
115
|
"Harness/scripts/wf-remove.mjs": "sha256-576bc92325bf63d4142eb6028d095f12abf9d845a1a178dfe247b813794476a9",
|
|
116
|
-
"Harness/scripts/wf-update-check.mjs": "sha256-
|
|
116
|
+
"Harness/scripts/wf-update-check.mjs": "sha256-40044032d6b4a3307e6dc5ea978d18c010936342d41f3324b3a2b92abe6646c4",
|
|
117
117
|
"Harness/subagents.md": "sha256-f2e81c21792b4d70c64c9bd22e9548d783d945dddb3504b4937e04ca1d04320d",
|
|
118
118
|
"Harness/templates/ACCEPTANCE.template.md": "sha256-394a6c08b155b1d121f1d51da774f7988bdb891224c6d5f2ca2c131e8c95ffb3",
|
|
119
119
|
"Harness/templates/API_CONTRACT.template.md": "sha256-e11ec9866576624982f35e77359e3f4ef3aea3e3f6b5895f6cc5419d29699fc5",
|
|
@@ -4,7 +4,7 @@ Run the Harness update checker script. Do not invoke a skill or start WF mode.
|
|
|
4
4
|
|
|
5
5
|
## Flow
|
|
6
6
|
|
|
7
|
-
1. Run `node Harness/scripts/wf-update-check.mjs --json` and use the `agent` block as the action plan.
|
|
7
|
+
1. Run `node Harness/scripts/wf-update-check.mjs --json` and use the `agent` block as the action plan. Current updaters try npm `create-harness-vibe-coding@latest` first, then the canonical GitHub source `LiWeny16/create-harness-vibe-coding`, then the legacy compatibility mirror `zingspark/create-harness-vibe-coding`.
|
|
8
8
|
2. Preserve all PRESERVE files. Never overwrite user task, memory, research, README, package, or architecture files.
|
|
9
9
|
3. If `agent.safeApplyCommand` is present, run it to apply SAFE/NEW files first.
|
|
10
10
|
4. For conflicts, compare local with `templateHint` or `remoteUrl`, decide merge/keep-local/overwrite, and record via `--accept-local`, `--accept-merged`, or `--accept-template`.
|
|
@@ -22,3 +22,9 @@ npx create-harness-vibe-coding@latest <project-name> . -y --on-conflict skip
|
|
|
22
22
|
```
|
|
23
23
|
|
|
24
24
|
This creates missing Harness files while preserving CLAUDE.md, README.md, tasks, memory, research, and all user data. After recovery, re-run the update check.
|
|
25
|
+
|
|
26
|
+
If an old updater reports only `0.8.10`, run the latest installer command above or re-run the checker with:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
node Harness/scripts/wf-update-check.mjs --json --source-base https://raw.githubusercontent.com/LiWeny16/create-harness-vibe-coding/main/templates/common/
|
|
30
|
+
```
|
|
@@ -109,7 +109,7 @@ When `--on-conflict skip` leaves existing files untouched, the script still owns
|
|
|
109
109
|
|
|
110
110
|
2. For each file in `agent.aiMergeRequired[]`, use its `templateHint` to locate the harness template counterpart only when semantic comparison is needed:
|
|
111
111
|
- From npm: `node_modules/create-harness-vibe-coding/<templateHint>`
|
|
112
|
-
- From GitHub: `https://raw.githubusercontent.com/
|
|
112
|
+
- From GitHub: `https://raw.githubusercontent.com/LiWeny16/create-harness-vibe-coding/main/<templateHint>`
|
|
113
113
|
|
|
114
114
|
3. For each skipped file:
|
|
115
115
|
- Read the existing project file.
|
|
@@ -24,6 +24,8 @@ WF-AUTO-SPARK is a `/wf` variant layered on WF-AUTO: it inherits the WF-KERNEL c
|
|
|
24
24
|
|
|
25
25
|
Accepted spark candidates re-enter the standard W2-W5 gates per [WF-KERNEL.md](WF-KERNEL.md): implementer (one file_claim, ≤3 files, ≤50 lines), verifier, reviewer(s), reflector. Spark searchers are read-only. Any implementation must use the dispatch packet from `Harness/dispatch.md` with explicit write set, forbidden truth files, AC IDs, and verification commands. A candidate with no verifier evidence or no reflector PASS is not accepted, even if the idea is valuable.
|
|
26
26
|
|
|
27
|
+
For task-state writes, CEO provides concise bullets and decisions; task-scribe formats task-state writes for roadmap, cycle PLAN, heartbeat, evidence pointers, and closeout. If task-scribe is unavailable, CEO may write the smallest durable checkpoint and must record that degradation in `PROGRESS.md`. This keeps the high-reasoning controller focused on decisions instead of spending tokens formatting process files.
|
|
28
|
+
|
|
27
29
|
## Startup: Roadmap Declaration
|
|
28
30
|
|
|
29
31
|
Before first spark cycle, CEO MUST declare a roadmap. This is written to `Harness/tasks/auto/SPARK-ROADMAP.md`.
|
|
@@ -89,12 +91,12 @@ STARTUP: Declare roadmap (North Star + Milestones) → user confirms
|
|
|
89
91
|
│ ↓ │
|
|
90
92
|
│ VERIFY + CROSS-REVIEW + REFLECTOR PASS │
|
|
91
93
|
│ ↓ │
|
|
92
|
-
│ VALUE REFLECTION: CEO writes
|
|
94
|
+
│ VALUE REFLECTION: CEO gives bullets; task-scribe writes PROGRESS.md │
|
|
93
95
|
│ "Driven by [source]. Matters because [reason]. │
|
|
94
96
|
│ Without this [consequence]. User notices [evidence]." │
|
|
95
97
|
│ ↓ │
|
|
96
98
|
│ MILESTONE CHECK: Is current milestone complete? │
|
|
97
|
-
│ Yes →
|
|
99
|
+
│ Yes → task-scribe updates SPARK-ROADMAP.md + PROGRESS │
|
|
98
100
|
│ No → continue │
|
|
99
101
|
│ ↓ │
|
|
100
102
|
│ RE-ANCHOR GATE (every 10 cycles): │
|
|
@@ -122,6 +124,13 @@ STARTUP: Declare roadmap (North Star + Milestones) → user confirms
|
|
|
122
124
|
| 7 | **Developer Experience** | WebSearch / Docs | New tooling, better CLI patterns, improved error messages |
|
|
123
125
|
| 8 | **Performance Benchmarks** | WebSearch | Industry benchmarks, optimization techniques, profiling tools |
|
|
124
126
|
|
|
127
|
+
### Spark Search Fallbacks
|
|
128
|
+
|
|
129
|
+
- If a researcher/search subagent is filtered, unavailable, or blocked by tool policy, CEO may run direct WebSearch/Tavily for discovery only and records `searchFallback: ceo-direct`.
|
|
130
|
+
- If a scoped skill name is unavailable, use the unscoped skill name plus a dispatch packet; if that still fails, continue with direct search and record the failure.
|
|
131
|
+
- Discovery fallback never authorizes source edits. Any accepted candidate still needs the standard implementer/verifier/reviewer/reflector chain.
|
|
132
|
+
- Search breadth is adaptive. Rotate source families and record skipped families; do not require a fixed count of external searches.
|
|
133
|
+
|
|
125
134
|
## Deviation Guard (50% Rule)
|
|
126
135
|
|
|
127
136
|
### What "50% deviation" means
|
|
@@ -161,7 +170,7 @@ Milestones CANNOT be changed without user confirmation IF:
|
|
|
161
170
|
|
|
162
171
|
## Value Reflection (per cycle)
|
|
163
172
|
|
|
164
|
-
After EVERY spark cycle, CEO writes:
|
|
173
|
+
After EVERY spark cycle, CEO gives the value-reflection bullets and task-scribe writes:
|
|
165
174
|
|
|
166
175
|
```text
|
|
167
176
|
## Cycle N — Value Reflection
|
|
@@ -177,6 +186,10 @@ Cumulative deviation (10-cycle): [X]%
|
|
|
177
186
|
|
|
178
187
|
If CEO CANNOT write a convincing value reflection → spark was NOT valuable → record as rejected and move on.
|
|
179
188
|
|
|
189
|
+
### Reflector Escalation
|
|
190
|
+
|
|
191
|
+
CEO may self-score low-risk candidates during pre-implementation triage, but final acceptance for an implemented spark still requires verifier evidence, cross-review, and reflector PASS. Use a reflector subagent for milestone completion, cumulative deviation concerns, high-risk changes, multi-file implementation, conflicting reviewer findings, or unclear value.
|
|
192
|
+
|
|
180
193
|
## Stop Condition
|
|
181
194
|
|
|
182
195
|
**Spark mode does NOT auto-stop.** Only these events stop it:
|
|
@@ -199,6 +212,10 @@ If CEO CANNOT write a convincing value reflection → spark was NOT valuable →
|
|
|
199
212
|
| SP4 | **Silent North Star drift** | Small changes accumulate, direction shifts without noticing | Cumulative deviation check every 10 cycles |
|
|
200
213
|
| SP5 | **Milestone rot** | Milestones become irrelevant but aren't updated | Milestone review at Re-Anchor Gate |
|
|
201
214
|
| SP6 | **Spark tunnel vision** | Only looking at one type of source | Rotate through the registered source families and record skipped sources |
|
|
215
|
+
| SP7 | **Literal anti-pattern matching** | A rule only catches one exact phrase and misses nearby violations | State the invariant and forbidden behavior, not just a magic string |
|
|
216
|
+
| SP8 | **Checksum drift after doc changes** | Root docs, templates, or `.harness-version` disagree after edits | Sync root + templates, then run `node scripts/build-version.mjs --check` |
|
|
217
|
+
| SP9 | **Process-file token burn** | CEO spends high-reasoning tokens formatting PLAN/PROGRESS/ROADMAP | CEO sends bullets; task-scribe formats task-state writes |
|
|
218
|
+
| SP10 | **Fake delegation** | In-process helper calls are logged as independent workers | Record channel fallback honestly; discovery fallback is not implementation delegation |
|
|
202
219
|
|
|
203
220
|
## Integration with /wf-auto
|
|
204
221
|
|
|
@@ -260,7 +277,7 @@ WF-AUTO-SPARK uses a dedicated task capsule at `Harness/tasks/auto/`. Shared wit
|
|
|
260
277
|
|
|
261
278
|
### PLAN.md Format (per cycle)
|
|
262
279
|
|
|
263
|
-
Before implementing, CEO
|
|
280
|
+
Before implementing, CEO gives one cycle-plan brief and task-scribe writes:
|
|
264
281
|
|
|
265
282
|
```markdown
|
|
266
283
|
# Cycle N Plan
|
|
@@ -26,9 +26,43 @@ WF-MAX is **explicit only**. Enter ONLY when the user explicitly types `/wf-max`
|
|
|
26
26
|
|
|
27
27
|
CEO reads, plans, dispatches, synthesizes, and writes task state only.
|
|
28
28
|
- **CEO never writes production source code.** All source edits are delegated to Workers.
|
|
29
|
-
- CEO
|
|
29
|
+
- CEO spawns task-scribe (haiku) by default to maintain dispatch ledger, heartbeat, and evidence pointers; if unavailable, CEO records the degradation and writes only the smallest durable checkpoint.
|
|
30
30
|
- CEO may spawn codebase-explorer(s) (haiku) for scoped source discovery.
|
|
31
31
|
|
|
32
|
+
## Worker Channel Degradation & Independence
|
|
33
|
+
|
|
34
|
+
WF-MAX Workers MUST execute as **independent agent contexts** — never as in-process tool calls from the CEO thread. When a worker channel is unavailable, degrade honestly; never disguise an in-process tool call as a Worker.
|
|
35
|
+
|
|
36
|
+
### Independence Levels
|
|
37
|
+
|
|
38
|
+
| Channel | Independence | Counts as Worker? |
|
|
39
|
+
|---|---|---|
|
|
40
|
+
| native subagent (Agent-tool dispatch) | independent | yes |
|
|
41
|
+
| peer CLI (`claude -p`, `codex exec`, `opencode run`) | independent (separate process) | yes |
|
|
42
|
+
| MCP tools (`mcp__codex.codex_implement`, `mcp__claude.claude_implement`, etc.) | inprocess (CEO thread) | NEVER |
|
|
43
|
+
|
|
44
|
+
### Degradation Chain
|
|
45
|
+
|
|
46
|
+
When delegating source edits to a Worker, try channels in order; descend on failure; when all independent channels are unavailable, stop honestly and ask the user — do NOT fall back to an in-process MCP tool:
|
|
47
|
+
|
|
48
|
+
0. **Probe first** — before dispatching any Worker, run `node Harness/scripts/probe-worker-channels.mjs` and read the live matrix (`Harness/tasks/auto/CHANNEL-MATRIX.md`). Degrade based on facts, not assumptions. Start the chain at the highest-priority channel the probe reports `available`.
|
|
49
|
+
1. native subagent (preferred — independent context, bounded writeSet)
|
|
50
|
+
2. `claude -p` peer CLI (independent process)
|
|
51
|
+
3. `codex exec` peer CLI (independent process)
|
|
52
|
+
4. all unavailable → honest pause + human escalation (record blocker in task PROGRESS.md)
|
|
53
|
+
|
|
54
|
+
### Hard Prohibition
|
|
55
|
+
|
|
56
|
+
`mcp__codex.codex_implement`, `mcp__claude.claude_implement`, and any CEO-thread MCP tool call MUST NOT be recorded or used as Worker execution. An in-process tool call has no independent context boundary and no enforced writeSet — using it and logging "CEO did not edit source" is fake compliance. Historical instance: `tasks/task-framework-metrics-and-entry-contract/PLAN.md:791`.
|
|
57
|
+
|
|
58
|
+
### Timeout & Retry
|
|
59
|
+
|
|
60
|
+
Every channel probe is bounded to ≤15s via `Promise.race` (see `probe-worker-channels.mjs`). A Worker dispatch that returns `unavailable-timeout` or `unavailable-error` (transient) is retried **once**; a second failure descends to the next channel in the chain. WF-MAX MUST NEVER hang on an unresponsive channel (historical failure: a 300s Codex read-only query hang, `tasks/task-framework-metrics-and-entry-contract/PLAN.md`). Bounded probes + single retry + honest descent replace silent hangs.
|
|
61
|
+
|
|
62
|
+
### Probe the current environment
|
|
63
|
+
|
|
64
|
+
Run `node Harness/scripts/probe-worker-channels.mjs` to get the live availability + independence matrix (see `tasks/auto/CHANNEL-MATRIX.md`). Degrade based on facts, not assumptions.
|
|
65
|
+
|
|
32
66
|
## Parallelism Priority
|
|
33
67
|
|
|
34
68
|
1. Read-only exploration — max parallel, all readSets
|
|
@@ -51,7 +85,7 @@ Manager_max = min(max(Manager_min * 2, Manager_min), 7)
|
|
|
51
85
|
- One file_claim per write Worker
|
|
52
86
|
- WriteSet must be disjoint across parallel Workers
|
|
53
87
|
- Workers return <=250 tokens + evidence/file paths
|
|
54
|
-
- task-scribe (haiku) runs alongside any wave
|
|
88
|
+
- task-scribe (haiku) runs alongside any wave when available; process-file delegation is the default, not a token-heavy CEO formatting task
|
|
55
89
|
|
|
56
90
|
## Token Budget and Fan-Out Caps
|
|
57
91
|
|
|
@@ -19,11 +19,16 @@ import { readFileSync, writeFileSync, existsSync, lstatSync, readdirSync, unlink
|
|
|
19
19
|
import { resolve, dirname, sep, join } from 'path';
|
|
20
20
|
import { fileURLToPath } from 'url';
|
|
21
21
|
import { createInterface } from 'readline';
|
|
22
|
+
import { gunzipSync } from 'zlib';
|
|
22
23
|
|
|
23
24
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
24
25
|
const ROOT = process.env.WF_ROOT ? resolve(process.env.WF_ROOT) : resolve(__dirname, '..', '..');
|
|
25
26
|
const VERSION_FILE = resolve(ROOT, 'Harness', '.harness-version');
|
|
26
|
-
const
|
|
27
|
+
const NPM_PACKAGE = 'create-harness-vibe-coding';
|
|
28
|
+
const NPM_REGISTRY = (process.env.NPM_CONFIG_REGISTRY || 'https://registry.npmjs.org').replace(/\/+$/, '');
|
|
29
|
+
const TEMPLATE_SUBPATH = 'templates/common/';
|
|
30
|
+
const DEFAULT_SOURCE_BASE = 'https://raw.githubusercontent.com/LiWeny16/create-harness-vibe-coding/main/templates/common/';
|
|
31
|
+
const LEGACY_SOURCE_BASE = 'https://raw.githubusercontent.com/zingspark/create-harness-vibe-coding/main/templates/common/';
|
|
27
32
|
|
|
28
33
|
// ── Classification constants ────────────────────────────────────────
|
|
29
34
|
|
|
@@ -151,6 +156,73 @@ async function fetchRemote(url, timeoutMs = 30000) {
|
|
|
151
156
|
}
|
|
152
157
|
}
|
|
153
158
|
|
|
159
|
+
async function fetchBytes(url, timeoutMs = 30000) {
|
|
160
|
+
const controller = new AbortController();
|
|
161
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
162
|
+
try {
|
|
163
|
+
const res = await fetch(url, { signal: controller.signal, headers: { 'User-Agent': 'harness-scan-clean' } });
|
|
164
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}: ${url}`);
|
|
165
|
+
return Buffer.from(await res.arrayBuffer());
|
|
166
|
+
} finally {
|
|
167
|
+
clearTimeout(timer);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function tarString(header, start, length) {
|
|
172
|
+
const slice = header.subarray(start, start + length);
|
|
173
|
+
const zero = slice.indexOf(0);
|
|
174
|
+
return Buffer.from(zero === -1 ? slice : slice.subarray(0, zero)).toString('utf8').trim();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function parseTarFiles(buffer) {
|
|
178
|
+
const files = new Map();
|
|
179
|
+
for (let offset = 0; offset + 512 <= buffer.length;) {
|
|
180
|
+
const header = buffer.subarray(offset, offset + 512);
|
|
181
|
+
if (header.every(byte => byte === 0)) break;
|
|
182
|
+
|
|
183
|
+
const name = tarString(header, 0, 100);
|
|
184
|
+
const prefix = tarString(header, 345, 155);
|
|
185
|
+
const size = Number.parseInt(tarString(header, 124, 12) || '0', 8) || 0;
|
|
186
|
+
const type = String.fromCharCode(header[156] || 0);
|
|
187
|
+
const fullName = prefix ? `${prefix}/${name}` : name;
|
|
188
|
+
const dataStart = offset + 512;
|
|
189
|
+
|
|
190
|
+
if ((type === '\0' || type === '0' || type === '') && fullName) {
|
|
191
|
+
files.set(fullName, buffer.subarray(dataStart, dataStart + size));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
offset = dataStart + Math.ceil(size / 512) * 512;
|
|
195
|
+
}
|
|
196
|
+
return files;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function fetchNpmLatestManifest() {
|
|
200
|
+
const meta = JSON.parse(await fetchRemote(`${NPM_REGISTRY}/${NPM_PACKAGE}/latest`, 15000));
|
|
201
|
+
const tarball = meta?.dist?.tarball;
|
|
202
|
+
if (!tarball) throw new Error(`npm metadata missing dist.tarball for ${NPM_PACKAGE}@latest`);
|
|
203
|
+
const files = parseTarFiles(gunzipSync(await fetchBytes(tarball, 30000)));
|
|
204
|
+
const manifest = files.get(`package/${TEMPLATE_SUBPATH}.harness-version`);
|
|
205
|
+
if (!manifest) throw new Error('npm package missing templates/common/.harness-version');
|
|
206
|
+
return Buffer.from(manifest).toString('utf8');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function fetchRemoteManifest(explicitSource) {
|
|
210
|
+
if (explicitSource) return fetchRemote(normalizeSourceBase(explicitSource) + '.harness-version');
|
|
211
|
+
try {
|
|
212
|
+
return await fetchNpmLatestManifest();
|
|
213
|
+
} catch (npmError) {
|
|
214
|
+
try {
|
|
215
|
+
return await fetchRemote(DEFAULT_SOURCE_BASE + '.harness-version');
|
|
216
|
+
} catch (canonicalError) {
|
|
217
|
+
try {
|
|
218
|
+
return await fetchRemote(LEGACY_SOURCE_BASE + '.harness-version');
|
|
219
|
+
} catch (legacyError) {
|
|
220
|
+
throw new Error(`npm latest: ${npmError.message}; canonical GitHub: ${canonicalError.message}; legacy mirror: ${legacyError.message}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
154
226
|
// ── Orphan detection ─────────────────────────────────────────────────
|
|
155
227
|
|
|
156
228
|
/**
|
|
@@ -285,7 +357,7 @@ async function main() {
|
|
|
285
357
|
const clean = args.includes('--clean');
|
|
286
358
|
const yes = args.includes('--yes');
|
|
287
359
|
const jsonOut = args.includes('--json');
|
|
288
|
-
const
|
|
360
|
+
const explicitSource = readFlagValue(args, '--source-base') || process.env.WF_SOURCE_BASE;
|
|
289
361
|
|
|
290
362
|
// 1. Read local state
|
|
291
363
|
if (!existsSync(VERSION_FILE)) {
|
|
@@ -314,7 +386,7 @@ async function main() {
|
|
|
314
386
|
// 2. Fetch remote version file
|
|
315
387
|
let remoteVersion;
|
|
316
388
|
try {
|
|
317
|
-
const raw = await
|
|
389
|
+
const raw = await fetchRemoteManifest(explicitSource);
|
|
318
390
|
if (isTemplate(raw)) {
|
|
319
391
|
if (jsonOut) {
|
|
320
392
|
console.log(JSON.stringify({ status: 'error', message: 'Remote .harness-version is a template (contains {{placeholders}}). Cannot determine dead files.' }));
|
|
@@ -328,9 +400,9 @@ async function main() {
|
|
|
328
400
|
remoteVersion = JSON.parse(raw);
|
|
329
401
|
} catch (e) {
|
|
330
402
|
if (jsonOut) {
|
|
331
|
-
console.log(JSON.stringify({ status: 'error', message: 'Cannot reach GitHub or
|
|
403
|
+
console.log(JSON.stringify({ status: 'error', message: 'Cannot reach npm, canonical GitHub, or legacy mirror update sources: ' + e.message }));
|
|
332
404
|
} else {
|
|
333
|
-
console.error('ERROR: Cannot reach GitHub or
|
|
405
|
+
console.error('ERROR: Cannot reach npm, canonical GitHub, or legacy mirror update sources. Offline?');
|
|
334
406
|
console.error(e.message);
|
|
335
407
|
}
|
|
336
408
|
process.exit(1);
|
|
@@ -325,6 +325,23 @@ for (const taskDir of taskDirs) {
|
|
|
325
325
|
}
|
|
326
326
|
}
|
|
327
327
|
|
|
328
|
+
// M2: MCP-as-Worker fake-compliance guard.
|
|
329
|
+
// Task capsules MUST NOT record mcp__codex.codex_implement / mcp__claude.claude_implement
|
|
330
|
+
// as Worker execution. Historical do-not-repeat references are allowed only when the
|
|
331
|
+
// file is marked ANTI-PATTERN. See Harness/WF-MAX.md "Worker Channel Degradation & Independence".
|
|
332
|
+
const MCP_AS_WORKER_RE = /mcp__(?:codex)\.codex_implement|mcp__(?:claude)\.claude_implement/;
|
|
333
|
+
for (const taskDir of taskDirs) {
|
|
334
|
+
if (!taskDir.startsWith('task-')) continue;
|
|
335
|
+
for (const f of ['PLAN.md', 'PROGRESS.md']) {
|
|
336
|
+
const rel = `Harness/tasks/${taskDir}/${f}`;
|
|
337
|
+
const text = read(rel);
|
|
338
|
+
if (!text) continue;
|
|
339
|
+
if (MCP_AS_WORKER_RE.test(text) && !/ANTI-PATTERN/i.test(text)) {
|
|
340
|
+
errors.push(`${rel} records mcp__*.implement as Worker execution (fake compliance; see WF-MAX.md "Worker Channel Degradation & Independence"). Mark historical references with "ANTI-PATTERN" or remove the tool call.`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
328
345
|
if (fs.existsSync(path.join(root, 'Harness/research/scaffolds.md'))) {
|
|
329
346
|
errors.push('legacy research file should be renamed: Harness/research/scaffolds.md -> Harness/research/research-results.md');
|
|
330
347
|
}
|
|
@@ -711,6 +728,12 @@ requireText('Harness/WF-AUTO.md', 'reflector PASS', 'wf-auto reflector gate');
|
|
|
711
728
|
requireText('Harness/WF-AUTO-SPARK.md', 'Inherited Execution Chain', 'wf-auto-spark inherited execution chain');
|
|
712
729
|
requireText('Harness/WF-AUTO-SPARK.md', 'External spark search replaces discovery only', 'wf-auto-spark discovery-only inheritance');
|
|
713
730
|
requireText('Harness/WF-AUTO-SPARK.md', 'reflector PASS', 'wf-auto-spark reflector gate');
|
|
731
|
+
requireText('Harness/WF-AUTO-SPARK.md', 'task-scribe formats task-state writes', 'wf-auto-spark task-scribe recorder delegation');
|
|
732
|
+
requireText('Harness/WF-AUTO-SPARK.md', 'searchFallback: ceo-direct', 'wf-auto-spark search fallback');
|
|
733
|
+
requireText('Harness/WF-AUTO-SPARK.md', 'pre-implementation triage', 'wf-auto-spark reflector escalation');
|
|
734
|
+
requireText('Harness/WF-AUTO-SPARK.md', 'Literal anti-pattern matching', 'wf-auto-spark anti-pattern invariant guard');
|
|
735
|
+
requireText('Harness/WF-AUTO-SPARK.md', 'node scripts/build-version.mjs --check', 'wf-auto-spark checksum drift guard');
|
|
736
|
+
requireText('Harness/WF-MAX.md', 'process-file delegation is the default', 'WF-MAX task-scribe process-file delegation default');
|
|
714
737
|
requireText('Harness/MEMORY_PROTOCOL.md', 'Scenario Memory Hints', 'memory protocol scenario hints');
|
|
715
738
|
requireText('Harness/MEMORY_PROTOCOL.md', 'WF closeout', 'memory protocol WF closeout injection row');
|
|
716
739
|
requireText('Harness/MEMORY_PROTOCOL.md', 'memory-master owns writes', 'memory protocol memory write ownership');
|
|
@@ -852,6 +875,7 @@ requireText('Harness/WF-KERNEL.md', 'small-fast', 'WF-KERNEL small-fast tier map
|
|
|
852
875
|
// WF variants reference WF-KERNEL
|
|
853
876
|
requireText('Harness/WF.md', 'WF-KERNEL.md', 'WF.md references WF-KERNEL');
|
|
854
877
|
requireText('Harness/WF-MAX.md', 'WF-KERNEL.md', 'WF-MAX.md references WF-KERNEL');
|
|
878
|
+
requireText('Harness/WF-MAX.md', 'probe-worker-channels.mjs', 'WF-MAX references worker-channel probe (M3 anti-regression)');
|
|
855
879
|
|
|
856
880
|
// Tier-aware acceptance: WF-Light must NOT require global cross-review/reflector
|
|
857
881
|
requireText('Harness/WF.md', 'Cross-review and reflector NOT mandatory', 'WF.md WF-Light no mandatory cross-review');
|
|
@@ -19,15 +19,22 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync, lstatSync } from 'f
|
|
|
19
19
|
import { createHash } from 'crypto';
|
|
20
20
|
import { resolve, dirname, sep } from 'path';
|
|
21
21
|
import { fileURLToPath } from 'url';
|
|
22
|
+
import { gunzipSync } from 'zlib';
|
|
22
23
|
|
|
23
24
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
24
25
|
const ROOT = process.env.WF_ROOT ? resolve(process.env.WF_ROOT) : resolve(__dirname, '..', '..');
|
|
25
26
|
const VERSION_FILE = resolve(ROOT, 'Harness', '.harness-version');
|
|
26
|
-
const
|
|
27
|
+
const NPM_PACKAGE = 'create-harness-vibe-coding';
|
|
28
|
+
const NPM_REGISTRY = (process.env.NPM_CONFIG_REGISTRY || 'https://registry.npmjs.org').replace(/\/+$/, '');
|
|
29
|
+
const GITHUB_REPO = 'LiWeny16/create-harness-vibe-coding';
|
|
30
|
+
const LEGACY_GITHUB_REPO = 'zingspark/create-harness-vibe-coding';
|
|
27
31
|
const GITHUB_LATEST_STABLE = `https://api.github.com/repos/${GITHUB_REPO}/releases/latest`;
|
|
28
32
|
const RAW_GITHUB = `https://raw.githubusercontent.com/${GITHUB_REPO}`;
|
|
33
|
+
const LEGACY_RAW_GITHUB = `https://raw.githubusercontent.com/${LEGACY_GITHUB_REPO}`;
|
|
29
34
|
const TEMPLATE_SUBPATH = 'templates/common/';
|
|
30
35
|
const DEFAULT_SOURCE_BASE = `${RAW_GITHUB}/main/${TEMPLATE_SUBPATH}`;
|
|
36
|
+
const LEGACY_SOURCE_BASE = `${LEGACY_RAW_GITHUB}/main/${TEMPLATE_SUBPATH}`;
|
|
37
|
+
const npmPackageCache = new Map();
|
|
31
38
|
|
|
32
39
|
// Tier classification
|
|
33
40
|
|
|
@@ -185,6 +192,47 @@ async function fetchRemote(url, timeoutMs = 30000) {
|
|
|
185
192
|
}
|
|
186
193
|
}
|
|
187
194
|
|
|
195
|
+
async function fetchBytes(url, timeoutMs = 30000) {
|
|
196
|
+
const controller = new AbortController();
|
|
197
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
198
|
+
try {
|
|
199
|
+
const res = await fetch(url, { signal: controller.signal, headers: { 'User-Agent': 'harness-wf-update-check' } });
|
|
200
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}: ${url}`);
|
|
201
|
+
return Buffer.from(await res.arrayBuffer());
|
|
202
|
+
} finally {
|
|
203
|
+
clearTimeout(timer);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function tarString(header, start, length) {
|
|
208
|
+
const slice = header.subarray(start, start + length);
|
|
209
|
+
const zero = slice.indexOf(0);
|
|
210
|
+
return Buffer.from(zero === -1 ? slice : slice.subarray(0, zero)).toString('utf8').trim();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function parseTarFiles(buffer) {
|
|
214
|
+
const files = new Map();
|
|
215
|
+
for (let offset = 0; offset + 512 <= buffer.length;) {
|
|
216
|
+
const header = buffer.subarray(offset, offset + 512);
|
|
217
|
+
if (header.every(byte => byte === 0)) break;
|
|
218
|
+
|
|
219
|
+
const name = tarString(header, 0, 100);
|
|
220
|
+
const prefix = tarString(header, 345, 155);
|
|
221
|
+
const sizeText = tarString(header, 124, 12);
|
|
222
|
+
const size = Number.parseInt(sizeText || '0', 8) || 0;
|
|
223
|
+
const type = String.fromCharCode(header[156] || 0);
|
|
224
|
+
const fullName = prefix ? `${prefix}/${name}` : name;
|
|
225
|
+
const dataStart = offset + 512;
|
|
226
|
+
|
|
227
|
+
if ((type === '\0' || type === '0' || type === '') && fullName) {
|
|
228
|
+
files.set(fullName, buffer.subarray(dataStart, dataStart + size));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
offset = dataStart + Math.ceil(size / 512) * 512;
|
|
232
|
+
}
|
|
233
|
+
return files;
|
|
234
|
+
}
|
|
235
|
+
|
|
188
236
|
// Main
|
|
189
237
|
|
|
190
238
|
function isPrerelease(v) {
|
|
@@ -192,7 +240,74 @@ function isPrerelease(v) {
|
|
|
192
240
|
return /-[0-9A-Za-z.-]+/.test(v.replace(/^[^0-9]*/, ''));
|
|
193
241
|
}
|
|
194
242
|
|
|
195
|
-
|
|
243
|
+
function parseSemver(v) {
|
|
244
|
+
if (!v || typeof v !== 'string') return [0, 0, 0];
|
|
245
|
+
return v.replace(/^[^0-9]*/, '').split('.').slice(0, 3).map(n => Number(n) || 0);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function cmpSemver(a, b) {
|
|
249
|
+
const va = parseSemver(a), vb = parseSemver(b);
|
|
250
|
+
for (let i = 0; i < 3; i++) {
|
|
251
|
+
if ((va[i] || 0) > (vb[i] || 0)) return 1;
|
|
252
|
+
if ((va[i] || 0) < (vb[i] || 0)) return -1;
|
|
253
|
+
}
|
|
254
|
+
return 0;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function loadNpmPackage(version = 'latest') {
|
|
258
|
+
const cacheKey = `${NPM_REGISTRY}:${version}`;
|
|
259
|
+
if (npmPackageCache.has(cacheKey)) return npmPackageCache.get(cacheKey);
|
|
260
|
+
|
|
261
|
+
const metaUrl = `${NPM_REGISTRY}/${NPM_PACKAGE}/${version}`;
|
|
262
|
+
const meta = JSON.parse(await fetchRemote(metaUrl, 15000));
|
|
263
|
+
const packageVersion = meta.version;
|
|
264
|
+
const tarball = meta?.dist?.tarball;
|
|
265
|
+
if (!packageVersion || !tarball) throw new Error(`npm metadata missing dist.tarball for ${NPM_PACKAGE}@${version}`);
|
|
266
|
+
|
|
267
|
+
const archive = gunzipSync(await fetchBytes(tarball, 30000));
|
|
268
|
+
const files = parseTarFiles(archive);
|
|
269
|
+
const loaded = { version: packageVersion, tarball, files };
|
|
270
|
+
npmPackageCache.set(cacheKey, loaded);
|
|
271
|
+
npmPackageCache.set(`${NPM_REGISTRY}:${packageVersion}`, loaded);
|
|
272
|
+
return loaded;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function readNpmFile(pkg, templateRelPath) {
|
|
276
|
+
const key = `package/${TEMPLATE_SUBPATH}${templateRelPath}`;
|
|
277
|
+
const content = pkg.files.get(key);
|
|
278
|
+
if (!content) throw new Error(`npm package missing ${key}`);
|
|
279
|
+
return Buffer.from(content).toString('utf8');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async function createNpmSource(version = 'latest') {
|
|
283
|
+
const pkg = await loadNpmPackage(version);
|
|
284
|
+
const raw = readNpmFile(pkg, '.harness-version');
|
|
285
|
+
if (isTemplate(raw)) throw new Error('npm .harness-version has not been generated yet');
|
|
286
|
+
const manifest = JSON.parse(raw);
|
|
287
|
+
return {
|
|
288
|
+
kind: 'npm',
|
|
289
|
+
stable: true,
|
|
290
|
+
label: `npm:${NPM_PACKAGE}@${pkg.version}/${TEMPLATE_SUBPATH}`,
|
|
291
|
+
version: pkg.version,
|
|
292
|
+
manifest,
|
|
293
|
+
package: pkg,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async function createUrlSource(sourceBase, { stable = false } = {}) {
|
|
298
|
+
const base = normalizeSourceBase(sourceBase);
|
|
299
|
+
const raw = await fetchRemote(base + '.harness-version');
|
|
300
|
+
if (isTemplate(raw)) throw new Error('remote .harness-version has not been generated yet');
|
|
301
|
+
return {
|
|
302
|
+
kind: 'url',
|
|
303
|
+
stable,
|
|
304
|
+
label: base,
|
|
305
|
+
base,
|
|
306
|
+
manifest: JSON.parse(raw),
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async function resolveGithubReleaseSource() {
|
|
196
311
|
try {
|
|
197
312
|
const controller = new AbortController();
|
|
198
313
|
const timer = setTimeout(() => controller.abort(), 15000);
|
|
@@ -209,26 +324,45 @@ async function resolveStableSourceBase() {
|
|
|
209
324
|
const data = JSON.parse(await res.text());
|
|
210
325
|
const tag = data && data.tag_name;
|
|
211
326
|
if (!tag || data.prerelease) return null;
|
|
212
|
-
|
|
213
|
-
const versionController = new AbortController();
|
|
214
|
-
const versionTimer = setTimeout(() => versionController.abort(), 15000);
|
|
215
|
-
try {
|
|
216
|
-
const versionRes = await fetch(source + '.harness-version', {
|
|
217
|
-
signal: versionController.signal,
|
|
218
|
-
headers: { 'User-Agent': 'harness-wf-update-check' },
|
|
219
|
-
});
|
|
220
|
-
if (!versionRes.ok) return null;
|
|
221
|
-
const raw = await versionRes.text();
|
|
222
|
-
if (isTemplate(raw)) return null;
|
|
223
|
-
} finally {
|
|
224
|
-
clearTimeout(versionTimer);
|
|
225
|
-
}
|
|
226
|
-
return source;
|
|
327
|
+
return await createUrlSource(`${RAW_GITHUB}/${tag}/${TEMPLATE_SUBPATH}`, { stable: true });
|
|
227
328
|
} catch {
|
|
228
329
|
return null;
|
|
229
330
|
}
|
|
230
331
|
}
|
|
231
332
|
|
|
333
|
+
async function resolveUpdateSource(explicitSource) {
|
|
334
|
+
if (explicitSource) {
|
|
335
|
+
return createUrlSource(explicitSource, { stable: false });
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const errors = [];
|
|
339
|
+
try {
|
|
340
|
+
return await createNpmSource('latest');
|
|
341
|
+
} catch (e) {
|
|
342
|
+
errors.push(`npm latest: ${e.message}`);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const releaseSource = await resolveGithubReleaseSource();
|
|
346
|
+
if (releaseSource) return releaseSource;
|
|
347
|
+
errors.push('GitHub latest release: unavailable');
|
|
348
|
+
|
|
349
|
+
try {
|
|
350
|
+
return await createUrlSource(DEFAULT_SOURCE_BASE, { stable: false });
|
|
351
|
+
} catch (e) {
|
|
352
|
+
errors.push(`GitHub canonical main: ${e.message}`);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
try {
|
|
356
|
+
return await createUrlSource(LEGACY_SOURCE_BASE, { stable: false });
|
|
357
|
+
} catch (e) {
|
|
358
|
+
errors.push(`GitHub legacy mirror: ${e.message}`);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const err = new Error('Cannot reach npm, canonical GitHub, or legacy mirror update sources.');
|
|
362
|
+
err.sourceErrors = errors;
|
|
363
|
+
throw err;
|
|
364
|
+
}
|
|
365
|
+
|
|
232
366
|
async function main() {
|
|
233
367
|
const args = process.argv.slice(2);
|
|
234
368
|
const apply = args.includes('--apply');
|
|
@@ -240,14 +374,16 @@ async function main() {
|
|
|
240
374
|
const acceptTemplate = readRepeatedFlagValues(args, '--accept-template');
|
|
241
375
|
const ignoreVersion = args.includes('--ignore-version') || args.includes('--force-check');
|
|
242
376
|
const explicitSource = readFlagValue(args, '--source-base') || process.env.WF_SOURCE_BASE;
|
|
243
|
-
let
|
|
244
|
-
let
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
377
|
+
let source;
|
|
378
|
+
let sourceBase;
|
|
379
|
+
let sourceStable = false;
|
|
380
|
+
let sourceErrors = [];
|
|
381
|
+
try {
|
|
382
|
+
source = await resolveUpdateSource(explicitSource);
|
|
383
|
+
sourceBase = source.label;
|
|
384
|
+
sourceStable = Boolean(source.stable);
|
|
385
|
+
} catch (e) {
|
|
386
|
+
sourceErrors = e.sourceErrors || [e.message];
|
|
251
387
|
}
|
|
252
388
|
|
|
253
389
|
// 1. Read local state
|
|
@@ -302,44 +438,23 @@ async function main() {
|
|
|
302
438
|
|
|
303
439
|
const localChecksums = localVersion.checksums || {};
|
|
304
440
|
|
|
305
|
-
// 2.
|
|
306
|
-
let remoteVersion;
|
|
307
|
-
|
|
308
|
-
const raw = await fetchRemote(sourceBase + '.harness-version');
|
|
309
|
-
if (isTemplate(raw)) {
|
|
310
|
-
if (jsonOut) {
|
|
311
|
-
console.log(JSON.stringify({ status: 'template-remote', message: 'Remote .harness-version has not been generated yet.' }));
|
|
312
|
-
} else {
|
|
313
|
-
console.log('WARN: Remote .harness-version is a template (contains {{placeholders}}).');
|
|
314
|
-
console.log(' The generate step has not been run on the remote repo. No update possible.');
|
|
315
|
-
console.log(' This is expected during development; the update mechanism works once the remote is live.');
|
|
316
|
-
}
|
|
317
|
-
process.exitCode = 1;
|
|
318
|
-
return;
|
|
319
|
-
}
|
|
320
|
-
remoteVersion = JSON.parse(raw);
|
|
321
|
-
} catch (e) {
|
|
441
|
+
// 2. Read remote version metadata from the selected update source.
|
|
442
|
+
let remoteVersion = source?.manifest;
|
|
443
|
+
if (!remoteVersion) {
|
|
322
444
|
if (jsonOut) {
|
|
323
|
-
console.log(JSON.stringify({
|
|
445
|
+
console.log(JSON.stringify({
|
|
446
|
+
status: 'offline',
|
|
447
|
+
message: 'Cannot reach npm, canonical GitHub, or legacy mirror update sources.',
|
|
448
|
+
sourceErrors,
|
|
449
|
+
}));
|
|
324
450
|
} else {
|
|
325
|
-
console.error('ERROR: Cannot reach GitHub or
|
|
326
|
-
console.error(
|
|
451
|
+
console.error('ERROR: Cannot reach npm, canonical GitHub, or legacy mirror update sources. Offline?');
|
|
452
|
+
for (const err of sourceErrors) console.error(` ${err}`);
|
|
327
453
|
}
|
|
328
454
|
process.exitCode = 1;
|
|
329
455
|
return;
|
|
330
456
|
}
|
|
331
457
|
|
|
332
|
-
// Compare versions and prevent downgrades from explicit custom sources.
|
|
333
|
-
function parseSemver(v) {
|
|
334
|
-
if (!v || typeof v !== 'string') return [0, 0, 0];
|
|
335
|
-
return v.replace(/^[^0-9]*/, '').split('.').slice(0, 3).map(n => Number(n) || 0);
|
|
336
|
-
}
|
|
337
|
-
function cmpSemver(a, b) {
|
|
338
|
-
const va = parseSemver(a), vb = parseSemver(b);
|
|
339
|
-
for (let i = 0; i < 3; i++) { if ((va[i]||0) > (vb[i]||0)) return 1; if ((va[i]||0) < (vb[i]||0)) return -1; }
|
|
340
|
-
return 0;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
458
|
const localGen = localVersion.generator || '0.0.0';
|
|
344
459
|
const remoteGen = remoteVersion.generator || '0.0.0';
|
|
345
460
|
|
|
@@ -355,7 +470,7 @@ async function main() {
|
|
|
355
470
|
const versionCmp = cmpSemver(remoteGen, localGen);
|
|
356
471
|
|
|
357
472
|
if (!ignoreVersion && versionCmp <= 0) {
|
|
358
|
-
const reportDowngrade = !
|
|
473
|
+
const reportDowngrade = !sourceStable;
|
|
359
474
|
if (jsonOut) {
|
|
360
475
|
console.log(JSON.stringify({
|
|
361
476
|
status: (versionCmp < 0 && reportDowngrade) ? 'downgrade-refused' : 'up-to-date',
|
|
@@ -385,13 +500,23 @@ async function main() {
|
|
|
385
500
|
return remoteSources[file] || file;
|
|
386
501
|
}
|
|
387
502
|
|
|
503
|
+
async function fetchSourceFile(templateRelPath) {
|
|
504
|
+
if (source.kind === 'npm') return readNpmFile(source.package, templateRelPath);
|
|
505
|
+
return fetchRemote(source.base + templateRelPath);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function sourceUrl(templateRelPath) {
|
|
509
|
+
if (source.kind === 'npm') return `${source.label}${templateRelPath}`;
|
|
510
|
+
return source.base + templateRelPath;
|
|
511
|
+
}
|
|
512
|
+
|
|
388
513
|
function withRemoteMeta(entry) {
|
|
389
514
|
if (!entry || !entry.file || !remoteChecksums[entry.file]) return entry;
|
|
390
515
|
const templateHint = remotePath(entry.file);
|
|
391
516
|
return {
|
|
392
517
|
...entry,
|
|
393
518
|
templateHint,
|
|
394
|
-
remoteUrl:
|
|
519
|
+
remoteUrl: sourceUrl(templateHint),
|
|
395
520
|
};
|
|
396
521
|
}
|
|
397
522
|
|
|
@@ -459,7 +584,7 @@ async function main() {
|
|
|
459
584
|
const dest = safePath(file);
|
|
460
585
|
if (!dest) throw new Error(`Traversal rejected: ${file}`);
|
|
461
586
|
if (existsSync(dest) && lstatSync(dest).isSymbolicLink()) throw new Error(`Symlink rejected: ${file}`);
|
|
462
|
-
const content = await
|
|
587
|
+
const content = await fetchSourceFile(remotePath(file));
|
|
463
588
|
const normalized = content.replace(/\r\n/g, '\n');
|
|
464
589
|
const fetchedHash = sha256(normalized);
|
|
465
590
|
if (fetchedHash !== remoteHash) throw new Error(`Hash mismatch: ${file}`);
|
|
@@ -678,56 +803,62 @@ async function main() {
|
|
|
678
803
|
console.log(`Applying SAFE/NEW files only; ${plan.conflict.length} conflicts will remain for AI/user merge.`);
|
|
679
804
|
}
|
|
680
805
|
|
|
681
|
-
const
|
|
682
|
-
let applied = 0;
|
|
806
|
+
const preparedWrites = [];
|
|
683
807
|
let failed = 0;
|
|
684
808
|
|
|
685
|
-
|
|
809
|
+
async function prepareWrite(entry, { mustNotExist = false } = {}) {
|
|
686
810
|
try {
|
|
687
|
-
const dest = safePath(
|
|
688
|
-
if (!dest) { console.error(` x Traversal rejected: ${
|
|
811
|
+
const dest = safePath(entry.file);
|
|
812
|
+
if (!dest) { console.error(` x Traversal rejected: ${entry.file}`); failed++; return; }
|
|
689
813
|
// Symlink rejection: do not follow symlinks.
|
|
690
|
-
if (
|
|
691
|
-
try { if (lstatSync(dest).isSymbolicLink()) { console.error(` x Symlink rejected: ${
|
|
814
|
+
if (existsSync(dest)) {
|
|
815
|
+
try { if (lstatSync(dest).isSymbolicLink()) { console.error(` x Symlink rejected: ${entry.file}`); failed++; return; } } catch (_) {}
|
|
816
|
+
if (mustNotExist) {
|
|
817
|
+
console.error(` x File created since plan: ${entry.file} - treating as CONFLICT`);
|
|
818
|
+
failed++; return;
|
|
819
|
+
}
|
|
692
820
|
}
|
|
693
|
-
const content = await
|
|
821
|
+
const content = await fetchSourceFile(remotePath(entry.file));
|
|
694
822
|
const normalized = content.replace(/\r\n/g, '\n');
|
|
695
823
|
const fetchedHash = sha256(normalized);
|
|
696
|
-
if (fetchedHash !==
|
|
697
|
-
console.error(` x Hash mismatch: ${
|
|
698
|
-
failed++;
|
|
824
|
+
if (fetchedHash !== entry.remoteHash) {
|
|
825
|
+
console.error(` x Hash mismatch: ${entry.file}`);
|
|
826
|
+
failed++; return;
|
|
699
827
|
}
|
|
700
|
-
|
|
701
|
-
writeFileSync(dest, normalized, 'utf-8');
|
|
702
|
-
applied++;
|
|
828
|
+
preparedWrites.push({ file: entry.file, dest, content: normalized, mustNotExist });
|
|
703
829
|
} catch (e) {
|
|
704
|
-
console.error(` x Failed: ${
|
|
830
|
+
console.error(` x Failed: ${entry.file} - ${e.message}`);
|
|
705
831
|
failed++;
|
|
706
832
|
}
|
|
707
833
|
}
|
|
708
834
|
|
|
835
|
+
for (const u of plan.updated) {
|
|
836
|
+
await prepareWrite(u);
|
|
837
|
+
}
|
|
838
|
+
|
|
709
839
|
for (const c of plan.created) {
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
console.error(` x File created since plan: ${
|
|
717
|
-
failed++;
|
|
840
|
+
await prepareWrite(c, { mustNotExist: true });
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
if (failed === 0) {
|
|
844
|
+
for (const prepared of preparedWrites) {
|
|
845
|
+
if (prepared.mustNotExist && existsSync(prepared.dest)) {
|
|
846
|
+
console.error(` x File created since plan: ${prepared.file} - treating as CONFLICT`);
|
|
847
|
+
failed++;
|
|
718
848
|
}
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
let applied = 0;
|
|
853
|
+
if (failed === 0) {
|
|
854
|
+
try {
|
|
855
|
+
for (const prepared of preparedWrites) {
|
|
856
|
+
mkdirSync(dirname(prepared.dest), { recursive: true });
|
|
857
|
+
writeFileSync(prepared.dest, prepared.content, 'utf-8');
|
|
858
|
+
applied++;
|
|
725
859
|
}
|
|
726
|
-
mkdirSync(dirname(dest), { recursive: true });
|
|
727
|
-
writeFileSync(dest, normalized, 'utf-8');
|
|
728
|
-
applied++;
|
|
729
860
|
} catch (e) {
|
|
730
|
-
console.error(` x Failed
|
|
861
|
+
console.error(` x Failed while writing prepared files - ${e.message}`);
|
|
731
862
|
failed++;
|
|
732
863
|
}
|
|
733
864
|
}
|