create-harness-vibe-coding 0.7.2 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README-CN.md +91 -155
- package/README.md +200 -177
- package/bin/create-harness-vibe-coding.js +2 -2
- package/package.json +45 -43
- package/src/generator.js +115 -77
- package/src/index.js +159 -49
- package/src/prompts.js +95 -27
- package/templates/common/.claude/agents/architect-manager.md +45 -45
- package/templates/common/.claude/agents/context-master.md +77 -77
- package/templates/common/.claude/agents/debugger.md +41 -41
- package/templates/common/.claude/agents/explore-manager.md +41 -41
- package/templates/common/.claude/agents/implement-manager.md +49 -49
- package/templates/common/.claude/agents/implementer.md +40 -40
- package/templates/common/.claude/agents/memory-master.md +66 -66
- package/templates/common/.claude/agents/planner.md +34 -34
- package/templates/common/.claude/agents/researcher.md +41 -41
- package/templates/common/.claude/agents/review-manager.md +56 -56
- package/templates/common/.claude/agents/test-writer.md +38 -38
- package/templates/common/.claude/agents/verifier.md +32 -32
- package/templates/common/.claude/rules/ecc/common.md +45 -45
- package/templates/common/.claude/settings.json +68 -68
- package/templates/common/.claude/skills/subagent-orchestrator/SKILL.md +36 -31
- package/templates/common/.claude/skills/wf/SKILL.md +36 -0
- package/templates/common/.claude/skills/wf-learn/SKILL.md +13 -28
- package/templates/common/.claude/skills/wf-max/SKILL.md +24 -88
- package/templates/common/.claude/skills/wf-readme/SKILL.md +49 -49
- package/templates/common/.claude/skills/wf-remove/SKILL.md +17 -38
- package/templates/common/.claude/skills/wf-review/SKILL.md +19 -69
- package/templates/common/.claude/skills/wf-update/SKILL.md +19 -61
- package/templates/common/.codex/config.toml +2 -2
- package/templates/common/.codex/hooks.json +37 -37
- package/templates/common/.harness-version +53 -51
- package/templates/common/AGENTS.md +30 -30
- package/templates/common/CLAUDE.md +94 -94
- package/templates/common/{docs/harness → Harness}/PROGRESS.md +17 -17
- package/templates/common/{docs → Harness}/README.md +18 -17
- package/templates/common/{docs/harness → Harness}/WF-MAX.md +223 -223
- package/templates/common/Harness/WF.md +217 -0
- package/templates/common/{docs/harness → Harness}/architecture.md +126 -126
- package/templates/common/{docs/harness → Harness}/dispatch.md +99 -99
- package/templates/common/{docs → Harness}/research/research-results.md +66 -66
- package/templates/common/{docs/harness → Harness}/subagents.md +194 -194
- package/templates/common/{docs → Harness}/tasks/_template/ARTIFACTS.md +3 -3
- package/templates/common/{docs → Harness}/tasks/_template/NOTES.md +3 -3
- package/templates/common/{docs → Harness}/tasks/_template/PLAN.md +40 -40
- package/templates/common/{docs → Harness}/tasks/_template/PROGRESS.md +29 -29
- package/templates/common/MEMORY.md +68 -66
- package/templates/common/README.md +44 -37
- package/templates/common/SETUP.md +41 -34
- package/templates/common/memory/agent-lessons-patterns.md +21 -21
- package/templates/common/memory/tool-usage-reflections.md +21 -21
- package/templates/common/memory/user-corrections-preferences.md +21 -21
- package/templates/common/scripts/scan-clean.mjs +450 -449
- package/templates/common/scripts/validate-harness.mjs +56 -34
- package/templates/common/scripts/wf-mode-hook.mjs +318 -318
- package/templates/common/scripts/wf-remove.mjs +400 -384
- package/templates/common/scripts/wf-statusline.ps1 +38 -38
- package/templates/common/scripts/wf-statusline.sh +48 -48
- package/templates/common/scripts/wf-update-check.mjs +389 -389
- package/templates/optional/catalog.json +63 -29
- package/templates/optional/skills/browser-e2e/.claude/skills/wf-browser/SKILL.md +194 -194
- package/templates/optional/skills/browser-e2e/{docs → Harness}/workflows/browser-e2e.md +75 -75
- package/templates/optional/skills/github-pr-review/{docs → Harness}/workflows/github-pr-review.md +28 -28
- package/templates/optional/skills/python-backend/{docs → Harness}/workflows/python-backend.md +34 -34
- package/templates/optional/skills/ts-react-frontend/{docs → Harness}/workflows/ts-react-frontend.md +35 -35
- package/templates/optional/skills/ui-ux-review/{docs → Harness}/workflows/ui-ux-review.md +26 -26
- package/templates/common/.claude/commands/wf-learn.md +0 -17
- package/templates/common/.claude/commands/wf-max.md +0 -49
- package/templates/common/.claude/commands/wf-remove.md +0 -23
- package/templates/common/.claude/commands/wf-review.md +0 -25
- package/templates/common/.claude/commands/wf-update.md +0 -17
- package/templates/common/.claude/commands/wf.md +0 -31
- package/templates/common/commands/wf-max.toml +0 -18
- package/templates/common/commands/wf-review.toml +0 -15
- package/templates/common/docs/harness/WF.md +0 -174
- package/templates/optional/skills/browser-e2e/.claude/commands/wf-browser.md +0 -25
- /package/templates/common/{docs/harness → Harness}/agent-workflow.md +0 -0
- /package/templates/common/{docs/harness → Harness}/context-loading.md +0 -0
- /package/templates/common/{docs/harness → Harness}/extension.md +0 -0
- /package/templates/common/{docs/harness → Harness}/lifecycle.md +0 -0
- /package/templates/common/{docs → Harness}/research/PRD.md +0 -0
- /package/templates/common/{docs → Harness}/research/README.md +0 -0
|
@@ -1,396 +1,412 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* wf-remove.mjs — Safely remove Harness framework files.
|
|
4
|
-
*
|
|
5
|
-
* Classifies all Harness-owned files into:
|
|
6
|
-
* SAFE — framework files matching stored checksums → auto-remove
|
|
7
|
-
* MODIFIED — framework files edited by user → MUST confirm
|
|
8
|
-
* USER — user data files → NEVER remove
|
|
9
|
-
*
|
|
10
|
-
* Usage:
|
|
11
|
-
* node Harness/scripts/wf-remove.mjs # dry-run: show plan
|
|
12
|
-
* node Harness/scripts/wf-remove.mjs --apply # apply SAFE, prompt for MODIFIED
|
|
13
|
-
* node Harness/scripts/wf-remove.mjs --yes # auto-yes for SAFE only (non-interactive)
|
|
14
|
-
* node Harness/scripts/wf-remove.mjs --json # JSON plan for AI consumption
|
|
15
|
-
* node Harness/scripts/wf-remove.mjs --apply --yes # auto-apply SAFE, skip MODIFIED, report
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { readFileSync, writeFileSync, existsSync, unlinkSync, rmdirSync, readdirSync, lstatSync } from 'fs';
|
|
19
|
-
import { createHash } from 'crypto';
|
|
20
|
-
import { resolve, dirname, join, sep } from 'path';
|
|
21
|
-
import { fileURLToPath } from 'url';
|
|
22
|
-
import { createInterface } from 'readline';
|
|
23
|
-
|
|
24
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
25
|
-
const ROOT = process.env.WF_ROOT
|
|
26
|
-
? resolve(process.env.WF_ROOT)
|
|
27
|
-
: (existsSync(resolve(process.cwd(), 'Harness', '.harness-version'))
|
|
28
|
-
? process.cwd()
|
|
29
|
-
: resolve(__dirname, '..', '..'));
|
|
30
|
-
const VERSION_FILE = resolve(ROOT, 'Harness', '.harness-version');
|
|
31
|
-
|
|
32
|
-
// ── Classification ─────────────────────────────────────────────────
|
|
33
|
-
|
|
34
|
-
/** Path prefixes that Harness framework is allowed to own. ANY file outside these is NEVER removed. */
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* wf-remove.mjs — Safely remove Harness framework files.
|
|
4
|
+
*
|
|
5
|
+
* Classifies all Harness-owned files into:
|
|
6
|
+
* SAFE — framework files matching stored checksums → auto-remove
|
|
7
|
+
* MODIFIED — framework files edited by user → MUST confirm
|
|
8
|
+
* USER — user data files → NEVER remove
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* node Harness/scripts/wf-remove.mjs # dry-run: show plan
|
|
12
|
+
* node Harness/scripts/wf-remove.mjs --apply # apply SAFE, prompt for MODIFIED
|
|
13
|
+
* node Harness/scripts/wf-remove.mjs --yes # auto-yes for SAFE only (non-interactive)
|
|
14
|
+
* node Harness/scripts/wf-remove.mjs --json # JSON plan for AI consumption
|
|
15
|
+
* node Harness/scripts/wf-remove.mjs --apply --yes # auto-apply SAFE, skip MODIFIED, report
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { readFileSync, writeFileSync, existsSync, unlinkSync, rmdirSync, readdirSync, lstatSync } from 'fs';
|
|
19
|
+
import { createHash } from 'crypto';
|
|
20
|
+
import { resolve, dirname, join, sep } from 'path';
|
|
21
|
+
import { fileURLToPath } from 'url';
|
|
22
|
+
import { createInterface } from 'readline';
|
|
23
|
+
|
|
24
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
const ROOT = process.env.WF_ROOT
|
|
26
|
+
? resolve(process.env.WF_ROOT)
|
|
27
|
+
: (existsSync(resolve(process.cwd(), 'Harness', '.harness-version'))
|
|
28
|
+
? process.cwd()
|
|
29
|
+
: resolve(__dirname, '..', '..'));
|
|
30
|
+
const VERSION_FILE = resolve(ROOT, 'Harness', '.harness-version');
|
|
31
|
+
|
|
32
|
+
// ── Classification ─────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
/** Path prefixes that Harness framework is allowed to own. ANY file outside these is NEVER removed. */
|
|
35
35
|
const HARNESS_PREFIXES = [
|
|
36
36
|
'.claude/',
|
|
37
|
+
'.agents/',
|
|
38
|
+
'.codex/',
|
|
37
39
|
'Harness/',
|
|
38
|
-
'CLAUDE.md',
|
|
39
|
-
'AGENTS.md',
|
|
40
|
-
'MEMORY.md',
|
|
41
|
-
'tests/.gitkeep',
|
|
42
|
-
];
|
|
43
|
-
|
|
44
|
-
/** Files the user owns — NEVER remove. Consistent with wf-update-check PRESERVE_PATTERNS. */
|
|
45
|
-
const USER_DATA_PATTERNS = [
|
|
46
|
-
/^Harness\/PROGRESS\.md$/,
|
|
47
|
-
/^Harness\/tasks\//,
|
|
48
|
-
/^Harness\/memory\//,
|
|
49
|
-
/^Harness\/research\/PRD\.md$/,
|
|
50
|
-
/^Harness\/research\/research-results\.md$/,
|
|
51
|
-
/^Harness\/architecture\.md$/,
|
|
52
|
-
/^Harness\/workflows\//,
|
|
53
|
-
/^Harness\/features\//,
|
|
54
|
-
/^Harness\/domain\//,
|
|
55
|
-
/^README\.md$/,
|
|
56
|
-
/^\.gitignore$/,
|
|
57
|
-
/^package\.json$/,
|
|
58
|
-
/^package-lock\.json$/,
|
|
59
|
-
];
|
|
60
|
-
|
|
61
|
-
function isUserData(file) {
|
|
62
|
-
return USER_DATA_PATTERNS.some(p => p.test(file));
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function isHarnessOwned(file) {
|
|
66
|
-
return HARNESS_PREFIXES.some(p => file.startsWith(p));
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** Framework files that should always exist (not part of removal). */
|
|
70
|
-
const KEEP_FRAMEWORK = new Set([
|
|
71
|
-
'Harness/.harness-version',
|
|
72
|
-
]);
|
|
73
|
-
|
|
74
|
-
/** Directories to clean up if empty after file removal. */
|
|
75
|
-
const CLEANUP_DIRS = [
|
|
76
|
-
'.claude/agents',
|
|
40
|
+
'CLAUDE.md',
|
|
41
|
+
'AGENTS.md',
|
|
42
|
+
'MEMORY.md',
|
|
43
|
+
'tests/.gitkeep',
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
/** Files the user owns — NEVER remove. Consistent with wf-update-check PRESERVE_PATTERNS. */
|
|
47
|
+
const USER_DATA_PATTERNS = [
|
|
48
|
+
/^Harness\/PROGRESS\.md$/,
|
|
49
|
+
/^Harness\/tasks\//,
|
|
50
|
+
/^Harness\/memory\//,
|
|
51
|
+
/^Harness\/research\/PRD\.md$/,
|
|
52
|
+
/^Harness\/research\/research-results\.md$/,
|
|
53
|
+
/^Harness\/architecture\.md$/,
|
|
54
|
+
/^Harness\/workflows\//,
|
|
55
|
+
/^Harness\/features\//,
|
|
56
|
+
/^Harness\/domain\//,
|
|
57
|
+
/^README\.md$/,
|
|
58
|
+
/^\.gitignore$/,
|
|
59
|
+
/^package\.json$/,
|
|
60
|
+
/^package-lock\.json$/,
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
function isUserData(file) {
|
|
64
|
+
return USER_DATA_PATTERNS.some(p => p.test(file));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function isHarnessOwned(file) {
|
|
68
|
+
return HARNESS_PREFIXES.some(p => file.startsWith(p));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Framework files that should always exist (not part of removal). */
|
|
72
|
+
const KEEP_FRAMEWORK = new Set([
|
|
73
|
+
'Harness/.harness-version',
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
/** Directories to clean up if empty after file removal. */
|
|
77
|
+
const CLEANUP_DIRS = [
|
|
78
|
+
'.claude/agents',
|
|
77
79
|
'.claude/skills/wf-auto',
|
|
78
80
|
'.claude/skills/wf-browser',
|
|
81
|
+
'.claude/skills/wf',
|
|
79
82
|
'.claude/skills/wf-learn',
|
|
80
|
-
'.claude/skills/wf-max',
|
|
81
|
-
'.claude/skills/wf-readme',
|
|
82
|
-
'.claude/skills/wf-review',
|
|
83
|
-
'.claude/skills/wf-update',
|
|
83
|
+
'.claude/skills/wf-max',
|
|
84
|
+
'.claude/skills/wf-readme',
|
|
85
|
+
'.claude/skills/wf-review',
|
|
86
|
+
'.claude/skills/wf-update',
|
|
84
87
|
'.claude/skills/wf-remove',
|
|
85
88
|
'.claude/skills/subagent-orchestrator',
|
|
86
|
-
'.
|
|
89
|
+
'.agents/skills/wf-auto',
|
|
90
|
+
'.agents/skills/wf-browser',
|
|
91
|
+
'.agents/skills/wf',
|
|
92
|
+
'.agents/skills/wf-learn',
|
|
93
|
+
'.agents/skills/wf-max',
|
|
94
|
+
'.agents/skills/wf-readme',
|
|
95
|
+
'.agents/skills/wf-review',
|
|
96
|
+
'.agents/skills/wf-update',
|
|
97
|
+
'.agents/skills/wf-remove',
|
|
98
|
+
'.agents/skills/subagent-orchestrator',
|
|
99
|
+
'.agents/skills',
|
|
100
|
+
'.agents',
|
|
101
|
+
'.codex',
|
|
87
102
|
'.claude/rules/ecc',
|
|
88
|
-
'.claude/rules',
|
|
89
|
-
'Harness/scripts',
|
|
90
|
-
'Harness/research',
|
|
91
|
-
'Harness/workflows',
|
|
92
|
-
'Harness/domain',
|
|
93
|
-
'Harness/features',
|
|
94
|
-
'Harness/memory',
|
|
95
|
-
'Harness/tasks/_template',
|
|
96
|
-
'Harness/tasks/auto',
|
|
97
|
-
'Harness/tasks',
|
|
98
|
-
'Harness',
|
|
99
|
-
];
|
|
100
|
-
|
|
101
|
-
// ── Helpers ────────────────────────────────────────────────────────
|
|
102
|
-
|
|
103
|
-
/** Reject paths that escape ROOT (traversal, absolute, .., etc.). Sync with wf-update-check. */
|
|
104
|
-
function safePath(file) {
|
|
105
|
-
let normalized = file.replace(/\\/g, '/').replace(/^\/+/, '');
|
|
106
|
-
if (normalized.includes('//')) return null; // double slash bypass
|
|
107
|
-
if (normalized.split('/').some(p => p === '..')) return null;
|
|
108
|
-
if (file.startsWith('/') || file.startsWith('\\')) return null;
|
|
109
|
-
if (normalized === '.' || normalized === '') return null;
|
|
110
|
-
const resolved = resolve(ROOT, normalized);
|
|
111
|
-
if (!resolved.startsWith(ROOT + sep) && resolved !== ROOT) return null;
|
|
112
|
-
return resolved;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function sha256File(path) {
|
|
116
|
-
if (!existsSync(path)) return null;
|
|
117
|
-
let content = readFileSync(path, 'utf-8');
|
|
118
|
-
content = content.replace(/\r\n/g, '\n');
|
|
119
|
-
return 'sha256-' + createHash('sha256').update(content).digest('hex');
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function removeEmptyDirs(startDir) {
|
|
123
|
-
if (!existsSync(startDir)) return;
|
|
124
|
-
try {
|
|
125
|
-
const entries = readdirSync(startDir);
|
|
126
|
-
for (const e of entries) {
|
|
127
|
-
const full = join(startDir, e);
|
|
128
|
-
if (lstatSync(full).isDirectory() && !lstatSync(full).isSymbolicLink()) removeEmptyDirs(full);
|
|
129
|
-
}
|
|
130
|
-
// After recursing, check if dir is now empty
|
|
131
|
-
const remaining = readdirSync(startDir);
|
|
132
|
-
if (remaining.length === 0) {
|
|
133
|
-
rmdirSync(startDir);
|
|
134
|
-
}
|
|
135
|
-
} catch (_) { /* permissions or already gone */ }
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
async function askUser(question) {
|
|
139
|
-
if (!process.stdin.isTTY) {
|
|
140
|
-
console.log(' (non-interactive — defaulting to KEEP)');
|
|
141
|
-
return 'k';
|
|
142
|
-
}
|
|
143
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
144
|
-
return new Promise(resolve => {
|
|
145
|
-
rl.question(question, answer => { rl.close(); resolve(answer.trim().toLowerCase()); });
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// ── Main ───────────────────────────────────────────────────────────
|
|
150
|
-
|
|
151
|
-
async function main() {
|
|
152
|
-
const args = process.argv.slice(2);
|
|
153
|
-
const apply = args.includes('--apply');
|
|
154
|
-
const yes = args.includes('--yes');
|
|
155
|
-
const jsonOut = args.includes('--json');
|
|
156
|
-
|
|
157
|
-
// 1. Build file inventory
|
|
158
|
-
const localVersion = existsSync(VERSION_FILE)
|
|
159
|
-
? JSON.parse(readFileSync(VERSION_FILE, 'utf-8'))
|
|
160
|
-
: { checksums: {} };
|
|
161
|
-
const storedChecksums = localVersion.checksums || {};
|
|
162
|
-
|
|
163
|
-
// All files from checksums + known framework patterns
|
|
164
|
-
const allFiles = new Set(Object.keys(storedChecksums));
|
|
165
|
-
|
|
166
|
-
// Add framework files that might not be in checksums (newer additions)
|
|
103
|
+
'.claude/rules',
|
|
104
|
+
'Harness/scripts',
|
|
105
|
+
'Harness/research',
|
|
106
|
+
'Harness/workflows',
|
|
107
|
+
'Harness/domain',
|
|
108
|
+
'Harness/features',
|
|
109
|
+
'Harness/memory',
|
|
110
|
+
'Harness/tasks/_template',
|
|
111
|
+
'Harness/tasks/auto',
|
|
112
|
+
'Harness/tasks',
|
|
113
|
+
'Harness',
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
// ── Helpers ────────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
/** Reject paths that escape ROOT (traversal, absolute, .., etc.). Sync with wf-update-check. */
|
|
119
|
+
function safePath(file) {
|
|
120
|
+
let normalized = file.replace(/\\/g, '/').replace(/^\/+/, '');
|
|
121
|
+
if (normalized.includes('//')) return null; // double slash bypass
|
|
122
|
+
if (normalized.split('/').some(p => p === '..')) return null;
|
|
123
|
+
if (file.startsWith('/') || file.startsWith('\\')) return null;
|
|
124
|
+
if (normalized === '.' || normalized === '') return null;
|
|
125
|
+
const resolved = resolve(ROOT, normalized);
|
|
126
|
+
if (!resolved.startsWith(ROOT + sep) && resolved !== ROOT) return null;
|
|
127
|
+
return resolved;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function sha256File(path) {
|
|
131
|
+
if (!existsSync(path)) return null;
|
|
132
|
+
let content = readFileSync(path, 'utf-8');
|
|
133
|
+
content = content.replace(/\r\n/g, '\n');
|
|
134
|
+
return 'sha256-' + createHash('sha256').update(content).digest('hex');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function removeEmptyDirs(startDir) {
|
|
138
|
+
if (!existsSync(startDir)) return;
|
|
139
|
+
try {
|
|
140
|
+
const entries = readdirSync(startDir);
|
|
141
|
+
for (const e of entries) {
|
|
142
|
+
const full = join(startDir, e);
|
|
143
|
+
if (lstatSync(full).isDirectory() && !lstatSync(full).isSymbolicLink()) removeEmptyDirs(full);
|
|
144
|
+
}
|
|
145
|
+
// After recursing, check if dir is now empty
|
|
146
|
+
const remaining = readdirSync(startDir);
|
|
147
|
+
if (remaining.length === 0) {
|
|
148
|
+
rmdirSync(startDir);
|
|
149
|
+
}
|
|
150
|
+
} catch (_) { /* permissions or already gone */ }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function askUser(question) {
|
|
154
|
+
if (!process.stdin.isTTY) {
|
|
155
|
+
console.log(' (non-interactive — defaulting to KEEP)');
|
|
156
|
+
return 'k';
|
|
157
|
+
}
|
|
158
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
159
|
+
return new Promise(resolve => {
|
|
160
|
+
rl.question(question, answer => { rl.close(); resolve(answer.trim().toLowerCase()); });
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ── Main ───────────────────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
async function main() {
|
|
167
|
+
const args = process.argv.slice(2);
|
|
168
|
+
const apply = args.includes('--apply');
|
|
169
|
+
const yes = args.includes('--yes');
|
|
170
|
+
const jsonOut = args.includes('--json');
|
|
171
|
+
|
|
172
|
+
// 1. Build file inventory
|
|
173
|
+
const localVersion = existsSync(VERSION_FILE)
|
|
174
|
+
? JSON.parse(readFileSync(VERSION_FILE, 'utf-8'))
|
|
175
|
+
: { checksums: {} };
|
|
176
|
+
const storedChecksums = localVersion.checksums || {};
|
|
177
|
+
|
|
178
|
+
// All files from checksums + known framework patterns
|
|
179
|
+
const allFiles = new Set(Object.keys(storedChecksums));
|
|
180
|
+
|
|
181
|
+
// Add framework files that might not be in checksums (newer additions)
|
|
167
182
|
const extraPatterns = [
|
|
183
|
+
'.claude/skills/wf/SKILL.md',
|
|
184
|
+
'.agents/skills/wf/SKILL.md',
|
|
168
185
|
'.claude/skills/wf-auto/SKILL.md',
|
|
169
|
-
'.
|
|
170
|
-
'.claude/commands/wf-auto-spark.md',
|
|
186
|
+
'.agents/skills/wf-auto/SKILL.md',
|
|
171
187
|
'Harness/WF-AUTO.md',
|
|
172
|
-
'Harness/tasks/auto/PROGRESS.md',
|
|
173
|
-
'Harness/tasks/auto/PLAN.md',
|
|
174
|
-
'Harness/scripts/wf-update-check.mjs',
|
|
175
|
-
'Harness/scripts/wf-remove.mjs',
|
|
176
|
-
];
|
|
177
|
-
for (const f of extraPatterns) {
|
|
178
|
-
if (existsSync(resolve(ROOT, f))) allFiles.add(f);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// 2. Classify every file
|
|
182
|
-
const safe = []; // SAFE — matches checksum, auto-remove
|
|
183
|
-
const modified = []; // MODIFIED — user edited, must confirm
|
|
184
|
-
const user = []; // USER — never remove
|
|
185
|
-
const skipped = []; // File not on disk, traversal rejected, or framework-keep
|
|
186
|
-
|
|
187
|
-
for (const file of [...allFiles].sort()) {
|
|
188
|
-
// Canonical normalization for classification
|
|
189
|
-
const canonical = file.replace(/\\/g, '/').replace(/\/+/g, '/');
|
|
190
|
-
|
|
191
|
-
if (KEEP_FRAMEWORK.has(canonical)) {
|
|
192
|
-
skipped.push({ file, reason: 'framework keep' });
|
|
193
|
-
continue;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
if (isUserData(canonical)) {
|
|
197
|
-
user.push({ file, reason: 'user data — NEVER removed' });
|
|
198
|
-
continue;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Only allow deletion of files under harness-owned prefixes
|
|
202
|
-
if (!isHarnessOwned(canonical)) {
|
|
203
|
-
user.push({ file, reason: 'outside harness prefix — NEVER removed' });
|
|
204
|
-
continue;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const diskPath = safePath(file);
|
|
208
|
-
if (!diskPath) {
|
|
209
|
-
skipped.push({ file, reason: 'path traversal rejected' });
|
|
210
|
-
continue;
|
|
211
|
-
}
|
|
212
|
-
if (!existsSync(diskPath)) {
|
|
213
|
-
skipped.push({ file, reason: 'not on disk' });
|
|
214
|
-
continue;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
const currentHash = sha256File(diskPath);
|
|
218
|
-
const storedHash = storedChecksums[file];
|
|
219
|
-
|
|
220
|
-
if (!storedHash) {
|
|
221
|
-
modified.push({ file, currentHash, storedHash: 'none', reason: 'not in checksums' });
|
|
222
|
-
} else if (currentHash === storedHash) {
|
|
223
|
-
safe.push({ file, currentHash, storedHash });
|
|
224
|
-
} else {
|
|
225
|
-
modified.push({ file, currentHash, storedHash, reason: 'user modified' });
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// Also check for .claude/settings.json (might have user hooks)
|
|
230
|
-
const settingsFile = resolve(ROOT, '.claude', 'settings.json');
|
|
231
|
-
if (existsSync(settingsFile)) {
|
|
232
|
-
const settingsHash = sha256File(settingsFile);
|
|
233
|
-
const storedSettingsHash = storedChecksums['.claude/settings.json'];
|
|
234
|
-
if (storedSettingsHash && settingsHash !== storedSettingsHash) {
|
|
235
|
-
// User modified settings — move to modified
|
|
236
|
-
// Remove from safe if it was there
|
|
237
|
-
const idx = safe.findIndex(s => s.file === '.claude/settings.json');
|
|
238
|
-
if (idx >= 0) {
|
|
239
|
-
safe.splice(idx, 1);
|
|
240
|
-
modified.push({ file: '.claude/settings.json', currentHash: settingsHash, storedHash: storedSettingsHash, reason: 'user modified settings (may contain hooks/permissions)' });
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// 3. Output plan
|
|
246
|
-
if (jsonOut) {
|
|
247
|
-
console.log(JSON.stringify({
|
|
248
|
-
safe: safe.map(s => s.file),
|
|
249
|
-
modified: modified.map(m => m.file),
|
|
250
|
-
user: user.map(u => u.file),
|
|
251
|
-
totalRemove: safe.length + modified.length,
|
|
252
|
-
totalSafe: safe.length,
|
|
253
|
-
totalModified: modified.length,
|
|
254
|
-
}, null, 2));
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
console.log('\n🧹 WF-REMOVE — Harness framework removal plan\n');
|
|
259
|
-
|
|
260
|
-
if (safe.length > 0) {
|
|
261
|
-
console.log(`✅ SAFE (${safe.length} files — auto-remove, unmodified framework):`);
|
|
262
|
-
for (const s of safe) console.log(` ✕ ${s.file}`);
|
|
263
|
-
console.log('');
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
if (modified.length > 0) {
|
|
267
|
-
console.log(`⚠ MODIFIED (${modified.length} files — REQUIRE CONFIRMATION):`);
|
|
268
|
-
for (const m of modified) {
|
|
269
|
-
console.log(` ? ${m.file} [${m.reason}]`);
|
|
270
|
-
}
|
|
271
|
-
console.log('');
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
if (user.length > 0) {
|
|
275
|
-
console.log(`🔒 USER DATA (${user.length} files — NEVER removed):`);
|
|
276
|
-
for (const u of user) console.log(` ○ ${u.file} [${u.reason}]`);
|
|
277
|
-
console.log('');
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
console.log(`Summary: ${safe.length} safe, ${modified.length} need confirm, ${user.length} preserved, ${skipped.length} skipped\n`);
|
|
281
|
-
|
|
282
|
-
if (!apply) {
|
|
283
|
-
console.log('DRY-RUN. Use --apply to execute the removal.\n');
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// 4. Apply — remove SAFE files automatically (rehash before unlink)
|
|
288
|
-
let safeRemoved = 0;
|
|
289
|
-
for (const s of safe) {
|
|
290
|
-
const diskPath = safePath(s.file);
|
|
291
|
-
if (!diskPath) { console.error(` ✗ Traversal rejected: ${s.file}`); continue; }
|
|
292
|
-
// Re-verify hash hasn't changed since classification
|
|
293
|
-
const currentHash = sha256File(diskPath);
|
|
294
|
-
if (currentHash !== s.storedHash) {
|
|
295
|
-
console.log(` ⊘ Skipped (modified since classification): ${s.file}`);
|
|
296
|
-
continue;
|
|
297
|
-
}
|
|
298
|
-
try {
|
|
299
|
-
unlinkSync(diskPath);
|
|
300
|
-
safeRemoved++;
|
|
301
|
-
} catch (e) {
|
|
302
|
-
console.error(` ✗ Failed to remove: ${s.file} — ${e.message}`);
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
console.log(`✓ Removed ${safeRemoved} safe files.`);
|
|
306
|
-
|
|
307
|
-
// 5. Handle MODIFIED files — prompt user
|
|
308
|
-
let modifiedRemoved = 0;
|
|
309
|
-
let modifiedKept = 0;
|
|
310
|
-
|
|
311
|
-
for (const m of modified) {
|
|
312
|
-
if (yes) {
|
|
313
|
-
// Non-interactive mode: skip all modified
|
|
314
|
-
console.log(` ⊘ Skipped (modified): ${m.file}`);
|
|
315
|
-
modifiedKept++;
|
|
316
|
-
continue;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
console.log(`\n─── ${m.file} ───`);
|
|
320
|
-
console.log(` Reason: ${m.reason}`);
|
|
321
|
-
console.log(` [D]elete [K]eep (default)`);
|
|
322
|
-
|
|
323
|
-
const answer = await askUser(' Choose [d/k]: ');
|
|
324
|
-
if (answer === 'd' || answer === 'delete') {
|
|
325
|
-
const diskPath = safePath(m.file);
|
|
326
|
-
if (!diskPath) { console.error(` ✗ Traversal rejected: ${m.file}`); continue; }
|
|
327
|
-
try {
|
|
328
|
-
unlinkSync(diskPath);
|
|
329
|
-
console.log(` ✓ Deleted: ${m.file}`);
|
|
330
|
-
modifiedRemoved++;
|
|
331
|
-
} catch (e) {
|
|
332
|
-
console.error(` ✗ Failed: ${m.file} — ${e.message}`);
|
|
333
|
-
}
|
|
334
|
-
} else {
|
|
335
|
-
console.log(` ⊘ Kept: ${m.file}`);
|
|
336
|
-
modifiedKept++;
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// 6. Cleanup empty directories
|
|
341
|
-
for (const dir of CLEANUP_DIRS) {
|
|
342
|
-
removeEmptyDirs(resolve(ROOT, dir));
|
|
343
|
-
}
|
|
344
|
-
// Remove root Harness/ if empty
|
|
345
|
-
removeEmptyDirs(resolve(ROOT, 'Harness'));
|
|
346
|
-
// Remove root .claude/ if empty (but this is rare)
|
|
347
|
-
removeEmptyDirs(resolve(ROOT, '.claude'));
|
|
348
|
-
|
|
349
|
-
// 7. Remove version file if everything is gone
|
|
350
|
-
if (existsSync(VERSION_FILE)) {
|
|
351
|
-
const harPath = resolve(ROOT, 'Harness');
|
|
352
|
-
if (!existsSync(harPath) || (existsSync(harPath) && readdirSync(harPath).filter(f => f !== '.harness-version').length === 0)) {
|
|
353
|
-
unlinkSync(VERSION_FILE);
|
|
354
|
-
console.log('✓ Removed .harness-version (Harness directory empty).');
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
// 7.5 Prune stale checksums for files that no longer exist on disk
|
|
359
|
-
if (existsSync(VERSION_FILE)) {
|
|
360
|
-
const versionData = JSON.parse(readFileSync(VERSION_FILE, 'utf-8'));
|
|
361
|
-
const checksums = versionData.checksums || {};
|
|
362
|
-
let pruned = 0;
|
|
363
|
-
for (const file of Object.keys(checksums)) {
|
|
364
|
-
const diskPath = safePath(file);
|
|
365
|
-
if (!diskPath || !existsSync(diskPath)) {
|
|
366
|
-
delete checksums[file];
|
|
367
|
-
pruned++;
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
if (pruned > 0) {
|
|
371
|
-
versionData.checksums = checksums;
|
|
372
|
-
writeFileSync(VERSION_FILE, JSON.stringify(versionData, null, 2) + '\n', 'utf-8');
|
|
373
|
-
console.log(`✓ Pruned ${pruned} stale checksum(s) from .harness-version.`);
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
// 8. Remove CLAUDE.md harness section if it only has harness binding
|
|
378
|
-
const claudePath = resolve(ROOT, 'CLAUDE.md');
|
|
379
|
-
if (existsSync(claudePath)) {
|
|
380
|
-
const content = readFileSync(claudePath, 'utf-8');
|
|
381
|
-
if (content.includes('Harness contract') || content.includes('Harness Binding')) {
|
|
382
|
-
const answer = yes ? 'k' : await askUser('\nCLAUDE.md contains Harness binding section. Remove it? [D]elete harness section / [K]eep as-is (default): ');
|
|
383
|
-
if (answer === 'd' || answer === 'delete') {
|
|
384
|
-
// Strip the harness section: everything from "## 1. Harness Binding" to the next "## "
|
|
385
|
-
const cleaned = content.replace(/## 1\. Harness Binding[\s\S]*?(?=## 2\.)/, '');
|
|
386
|
-
writeFileSync(claudePath, cleaned, 'utf-8');
|
|
387
|
-
console.log('✓ Stripped Harness binding section from CLAUDE.md.');
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
console.log(`\n🏁 Done. Safe: ${safeRemoved} removed. Modified: ${modifiedRemoved} removed, ${modifiedKept} kept.`);
|
|
393
|
-
console.log(' Run `git status` to review changes.\n');
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
main().catch(e => { console.error(e); process.exit(1); });
|
|
188
|
+
'Harness/tasks/auto/PROGRESS.md',
|
|
189
|
+
'Harness/tasks/auto/PLAN.md',
|
|
190
|
+
'Harness/scripts/wf-update-check.mjs',
|
|
191
|
+
'Harness/scripts/wf-remove.mjs',
|
|
192
|
+
];
|
|
193
|
+
for (const f of extraPatterns) {
|
|
194
|
+
if (existsSync(resolve(ROOT, f))) allFiles.add(f);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 2. Classify every file
|
|
198
|
+
const safe = []; // SAFE — matches checksum, auto-remove
|
|
199
|
+
const modified = []; // MODIFIED — user edited, must confirm
|
|
200
|
+
const user = []; // USER — never remove
|
|
201
|
+
const skipped = []; // File not on disk, traversal rejected, or framework-keep
|
|
202
|
+
|
|
203
|
+
for (const file of [...allFiles].sort()) {
|
|
204
|
+
// Canonical normalization for classification
|
|
205
|
+
const canonical = file.replace(/\\/g, '/').replace(/\/+/g, '/');
|
|
206
|
+
|
|
207
|
+
if (KEEP_FRAMEWORK.has(canonical)) {
|
|
208
|
+
skipped.push({ file, reason: 'framework keep' });
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (isUserData(canonical)) {
|
|
213
|
+
user.push({ file, reason: 'user data — NEVER removed' });
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Only allow deletion of files under harness-owned prefixes
|
|
218
|
+
if (!isHarnessOwned(canonical)) {
|
|
219
|
+
user.push({ file, reason: 'outside harness prefix — NEVER removed' });
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const diskPath = safePath(file);
|
|
224
|
+
if (!diskPath) {
|
|
225
|
+
skipped.push({ file, reason: 'path traversal rejected' });
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (!existsSync(diskPath)) {
|
|
229
|
+
skipped.push({ file, reason: 'not on disk' });
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const currentHash = sha256File(diskPath);
|
|
234
|
+
const storedHash = storedChecksums[file];
|
|
235
|
+
|
|
236
|
+
if (!storedHash) {
|
|
237
|
+
modified.push({ file, currentHash, storedHash: 'none', reason: 'not in checksums' });
|
|
238
|
+
} else if (currentHash === storedHash) {
|
|
239
|
+
safe.push({ file, currentHash, storedHash });
|
|
240
|
+
} else {
|
|
241
|
+
modified.push({ file, currentHash, storedHash, reason: 'user modified' });
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Also check for .claude/settings.json (might have user hooks)
|
|
246
|
+
const settingsFile = resolve(ROOT, '.claude', 'settings.json');
|
|
247
|
+
if (existsSync(settingsFile)) {
|
|
248
|
+
const settingsHash = sha256File(settingsFile);
|
|
249
|
+
const storedSettingsHash = storedChecksums['.claude/settings.json'];
|
|
250
|
+
if (storedSettingsHash && settingsHash !== storedSettingsHash) {
|
|
251
|
+
// User modified settings — move to modified
|
|
252
|
+
// Remove from safe if it was there
|
|
253
|
+
const idx = safe.findIndex(s => s.file === '.claude/settings.json');
|
|
254
|
+
if (idx >= 0) {
|
|
255
|
+
safe.splice(idx, 1);
|
|
256
|
+
modified.push({ file: '.claude/settings.json', currentHash: settingsHash, storedHash: storedSettingsHash, reason: 'user modified settings (may contain hooks/permissions)' });
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// 3. Output plan
|
|
262
|
+
if (jsonOut) {
|
|
263
|
+
console.log(JSON.stringify({
|
|
264
|
+
safe: safe.map(s => s.file),
|
|
265
|
+
modified: modified.map(m => m.file),
|
|
266
|
+
user: user.map(u => u.file),
|
|
267
|
+
totalRemove: safe.length + modified.length,
|
|
268
|
+
totalSafe: safe.length,
|
|
269
|
+
totalModified: modified.length,
|
|
270
|
+
}, null, 2));
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
console.log('\n🧹 WF-REMOVE — Harness framework removal plan\n');
|
|
275
|
+
|
|
276
|
+
if (safe.length > 0) {
|
|
277
|
+
console.log(`✅ SAFE (${safe.length} files — auto-remove, unmodified framework):`);
|
|
278
|
+
for (const s of safe) console.log(` ✕ ${s.file}`);
|
|
279
|
+
console.log('');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (modified.length > 0) {
|
|
283
|
+
console.log(`⚠ MODIFIED (${modified.length} files — REQUIRE CONFIRMATION):`);
|
|
284
|
+
for (const m of modified) {
|
|
285
|
+
console.log(` ? ${m.file} [${m.reason}]`);
|
|
286
|
+
}
|
|
287
|
+
console.log('');
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (user.length > 0) {
|
|
291
|
+
console.log(`🔒 USER DATA (${user.length} files — NEVER removed):`);
|
|
292
|
+
for (const u of user) console.log(` ○ ${u.file} [${u.reason}]`);
|
|
293
|
+
console.log('');
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
console.log(`Summary: ${safe.length} safe, ${modified.length} need confirm, ${user.length} preserved, ${skipped.length} skipped\n`);
|
|
297
|
+
|
|
298
|
+
if (!apply) {
|
|
299
|
+
console.log('DRY-RUN. Use --apply to execute the removal.\n');
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// 4. Apply — remove SAFE files automatically (rehash before unlink)
|
|
304
|
+
let safeRemoved = 0;
|
|
305
|
+
for (const s of safe) {
|
|
306
|
+
const diskPath = safePath(s.file);
|
|
307
|
+
if (!diskPath) { console.error(` ✗ Traversal rejected: ${s.file}`); continue; }
|
|
308
|
+
// Re-verify hash hasn't changed since classification
|
|
309
|
+
const currentHash = sha256File(diskPath);
|
|
310
|
+
if (currentHash !== s.storedHash) {
|
|
311
|
+
console.log(` ⊘ Skipped (modified since classification): ${s.file}`);
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
try {
|
|
315
|
+
unlinkSync(diskPath);
|
|
316
|
+
safeRemoved++;
|
|
317
|
+
} catch (e) {
|
|
318
|
+
console.error(` ✗ Failed to remove: ${s.file} — ${e.message}`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
console.log(`✓ Removed ${safeRemoved} safe files.`);
|
|
322
|
+
|
|
323
|
+
// 5. Handle MODIFIED files — prompt user
|
|
324
|
+
let modifiedRemoved = 0;
|
|
325
|
+
let modifiedKept = 0;
|
|
326
|
+
|
|
327
|
+
for (const m of modified) {
|
|
328
|
+
if (yes) {
|
|
329
|
+
// Non-interactive mode: skip all modified
|
|
330
|
+
console.log(` ⊘ Skipped (modified): ${m.file}`);
|
|
331
|
+
modifiedKept++;
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
console.log(`\n─── ${m.file} ───`);
|
|
336
|
+
console.log(` Reason: ${m.reason}`);
|
|
337
|
+
console.log(` [D]elete [K]eep (default)`);
|
|
338
|
+
|
|
339
|
+
const answer = await askUser(' Choose [d/k]: ');
|
|
340
|
+
if (answer === 'd' || answer === 'delete') {
|
|
341
|
+
const diskPath = safePath(m.file);
|
|
342
|
+
if (!diskPath) { console.error(` ✗ Traversal rejected: ${m.file}`); continue; }
|
|
343
|
+
try {
|
|
344
|
+
unlinkSync(diskPath);
|
|
345
|
+
console.log(` ✓ Deleted: ${m.file}`);
|
|
346
|
+
modifiedRemoved++;
|
|
347
|
+
} catch (e) {
|
|
348
|
+
console.error(` ✗ Failed: ${m.file} — ${e.message}`);
|
|
349
|
+
}
|
|
350
|
+
} else {
|
|
351
|
+
console.log(` ⊘ Kept: ${m.file}`);
|
|
352
|
+
modifiedKept++;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// 6. Cleanup empty directories
|
|
357
|
+
for (const dir of CLEANUP_DIRS) {
|
|
358
|
+
removeEmptyDirs(resolve(ROOT, dir));
|
|
359
|
+
}
|
|
360
|
+
// Remove root Harness/ if empty
|
|
361
|
+
removeEmptyDirs(resolve(ROOT, 'Harness'));
|
|
362
|
+
// Remove root .claude/ if empty (but this is rare)
|
|
363
|
+
removeEmptyDirs(resolve(ROOT, '.claude'));
|
|
364
|
+
|
|
365
|
+
// 7. Remove version file if everything is gone
|
|
366
|
+
if (existsSync(VERSION_FILE)) {
|
|
367
|
+
const harPath = resolve(ROOT, 'Harness');
|
|
368
|
+
if (!existsSync(harPath) || (existsSync(harPath) && readdirSync(harPath).filter(f => f !== '.harness-version').length === 0)) {
|
|
369
|
+
unlinkSync(VERSION_FILE);
|
|
370
|
+
console.log('✓ Removed .harness-version (Harness directory empty).');
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// 7.5 Prune stale checksums for files that no longer exist on disk
|
|
375
|
+
if (existsSync(VERSION_FILE)) {
|
|
376
|
+
const versionData = JSON.parse(readFileSync(VERSION_FILE, 'utf-8'));
|
|
377
|
+
const checksums = versionData.checksums || {};
|
|
378
|
+
let pruned = 0;
|
|
379
|
+
for (const file of Object.keys(checksums)) {
|
|
380
|
+
const diskPath = safePath(file);
|
|
381
|
+
if (!diskPath || !existsSync(diskPath)) {
|
|
382
|
+
delete checksums[file];
|
|
383
|
+
pruned++;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
if (pruned > 0) {
|
|
387
|
+
versionData.checksums = checksums;
|
|
388
|
+
writeFileSync(VERSION_FILE, JSON.stringify(versionData, null, 2) + '\n', 'utf-8');
|
|
389
|
+
console.log(`✓ Pruned ${pruned} stale checksum(s) from .harness-version.`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// 8. Remove CLAUDE.md harness section if it only has harness binding
|
|
394
|
+
const claudePath = resolve(ROOT, 'CLAUDE.md');
|
|
395
|
+
if (existsSync(claudePath)) {
|
|
396
|
+
const content = readFileSync(claudePath, 'utf-8');
|
|
397
|
+
if (content.includes('Harness contract') || content.includes('Harness Binding')) {
|
|
398
|
+
const answer = yes ? 'k' : await askUser('\nCLAUDE.md contains Harness binding section. Remove it? [D]elete harness section / [K]eep as-is (default): ');
|
|
399
|
+
if (answer === 'd' || answer === 'delete') {
|
|
400
|
+
// Strip the harness section: everything from "## 1. Harness Binding" to the next "## "
|
|
401
|
+
const cleaned = content.replace(/## 1\. Harness Binding[\s\S]*?(?=## 2\.)/, '');
|
|
402
|
+
writeFileSync(claudePath, cleaned, 'utf-8');
|
|
403
|
+
console.log('✓ Stripped Harness binding section from CLAUDE.md.');
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
console.log(`\n🏁 Done. Safe: ${safeRemoved} removed. Modified: ${modifiedRemoved} removed, ${modifiedKept} kept.`);
|
|
409
|
+
console.log(' Run `git status` to review changes.\n');
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
main().catch(e => { console.error(e); process.exit(1); });
|