46afea83c2
管道本來就是好的(install-harness 功能完整、冪等),**過時的是內容**:
harness skill(4066B)grep「意圖」「>>」= 0 命中,只講世界觀/別寫 Python
⇒ 新裝的封測者拿不到步驟 1 的核心教材(`>>` 意圖語法)。
■ 單一真相源:harness skill 改為建置期由 registry 複製
build-harness-skill.mjs=head + registry/skills/write_intent_workflow.md 正文 + tail。
選「建置期複製」的理由:npm files 只收 harness/,registry 不進套件;
symlink 在 npm pack 與 Windows 不可靠。產物 commit 進 repo(npm 裝的是產物、不跑 build)。
head/tail 是 harness 專屬(CLI 語境入口/acr 指令表/暴露同意/誠實鐵律),
install-harness 的 copyTree 跳過 .head/.tail,不鋪進使用者專案。
■ 其餘三件逐份對照現世代事實後更新(過時的直接刪,不留死代碼)
- CLAUDE.block.md:補 >> 意圖語法、not_found 兩條路、零件 vs recipe 分型、
腹語術紅線、金鑰只拿名字
- commands/arcrun.md:步驟改成「先寫意圖 → 丟去查 → 再寫 YAML」,補 acr search/validate
- hooks/arcrun-guard.sh:**正路提示改為指向 arcrun-mindset Skill +意圖語法**
(呼應「hook 沒提 skill 反而把 AI 導向 repo 文件」的教訓);
新增 code 節點腹語術提醒,settings.fragment 補 Write|Edit|MultiEdit matcher
■ 世代閘(防再度脫節)
check-harness-generation.mjs 檢查四件交付物的現世代指紋,缺指紋 exit 1,
掛進 npm run build(prepublishOnly 因此也擋)。
反向驗證:把 skill/CLAUDE.block 換回上一代 → 兩者都被擋下並逐條點名缺哪個指紋。
■ 驗收(考生 haiku/受測物=環境)
乾淨臨時目錄 acr install-harness → 四件鋪好;重跑冪等(全檔 md5 不變、
CLAUDE.md 66 行不變、hooks 條目 2 不變、arcrun 區塊仍 1 個)。
haiku 只讀該目錄的 CLAUDE.md+SKILL.md(明令禁讀 ~/.claude、禁上網;
兩份教材 md5 與大小均不同,可證非考本機那支)答十題
→ grade-step1.sh **10 / 10 通過**(判分器同時反向驗證仍會抓
ON_TRUE/ON_FAILURE/第一節點非 input)。
npm test 18/18、tsc 綠。
SDD:workflow-discovery/tasks.md 3.11
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
135 lines
5.9 KiB
TypeScript
135 lines
5.9 KiB
TypeScript
/**
|
||
* acr install-harness — 把「用戶 CC harness」裝進當前專案。
|
||
*
|
||
* 對象:在「用 arcrun 開發」的專案裡工作的 CC + 使用者。讓使用者的 CC 自動載入 arcrun 防護:
|
||
* - CLAUDE.md 區塊(事前提醒:別自寫 Python)
|
||
* - .claude/skills/arcrun-mindset/(世界觀 + 資源去哪取)
|
||
* - .claude/commands/arcrun.md(/arcrun slash command)
|
||
* - .claude/hooks/arcrun-guard.sh + settings.json(做錯被糾正)
|
||
*
|
||
* 冪等:重裝不重複、不破壞使用者既有 CLAUDE.md / settings。
|
||
* SDD:.agents/specs/user-cc-harness/design.md §2
|
||
*/
|
||
import { fileURLToPath } from 'node:url';
|
||
import {
|
||
readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync, chmodSync, readdirSync,
|
||
} from 'node:fs';
|
||
import { join, dirname } from 'node:path';
|
||
import chalk from 'chalk';
|
||
|
||
/** harness 素材根目錄(內嵌 npm 套件,SSOT=cli/harness/)。
|
||
* build 後此檔在 dist/commands/,harness/ 在套件根 → ../../harness。 */
|
||
function harnessRoot(): string {
|
||
const here = dirname(fileURLToPath(import.meta.url)); // .../dist/commands
|
||
return join(here, '..', '..', 'harness'); // .../harness
|
||
}
|
||
|
||
const START = '<!-- arcrun-harness:start -->';
|
||
const END = '<!-- arcrun-harness:end -->';
|
||
|
||
export async function cmdInstallHarness(): Promise<void> {
|
||
const cwd = process.cwd();
|
||
const src = harnessRoot();
|
||
|
||
if (!existsSync(src)) {
|
||
console.error(chalk.red(`找不到 harness 素材(${src})。套件安裝可能不完整,請重裝 arcrun。`));
|
||
process.exit(1);
|
||
}
|
||
|
||
console.log(chalk.bold('\n 安裝 arcrun harness 到當前專案\n'));
|
||
|
||
// 1. CLAUDE.md:append/取代 arcrun 區塊(標記包夾,冪等)
|
||
installClaudeBlock(cwd, src);
|
||
// 2. mindset Skill
|
||
copyTree(join(src, 'skills'), join(cwd, '.claude', 'skills'));
|
||
console.log(chalk.green(' ✓ .claude/skills/arcrun-mindset/'));
|
||
// 3. /arcrun command
|
||
copyTree(join(src, 'commands'), join(cwd, '.claude', 'commands'));
|
||
console.log(chalk.green(' ✓ .claude/commands/arcrun.md'));
|
||
// 4. hook
|
||
const hookDst = join(cwd, '.claude', 'hooks', 'arcrun-guard.sh');
|
||
mkdirSync(dirname(hookDst), { recursive: true });
|
||
copyFileSync(join(src, 'hooks', 'arcrun-guard.sh'), hookDst);
|
||
chmodSync(hookDst, 0o755);
|
||
console.log(chalk.green(' ✓ .claude/hooks/arcrun-guard.sh'));
|
||
// 5. settings.json:合併 hook 註冊(不覆蓋使用者既有設定)
|
||
mergeSettings(cwd, src);
|
||
console.log(chalk.green(' ✓ .claude/settings.json(已合併 arcrun guard hook)'));
|
||
|
||
console.log(chalk.gray('\n 提示:'));
|
||
console.log(chalk.gray(' • 首次在此專案開 Claude Code 會要求「信任工作區」,按信任 hook 才生效。'));
|
||
console.log(chalk.gray(' • 之後跟 CC 說需求即可(或打 /arcrun <你的需求>)。'));
|
||
console.log(chalk.gray(' • CC 偏好 MCP?可另跑 acr update 連 arcrun MCP(MCP 對齊中,optional)。\n'));
|
||
}
|
||
|
||
/** CLAUDE.md:無→建;有 arcrun 區塊→取代;有但無區塊→append。標記包夾,冪等。 */
|
||
function installClaudeBlock(cwd: string, src: string): void {
|
||
const block = readFileSync(join(src, 'CLAUDE.block.md'), 'utf8').trim();
|
||
const path = join(cwd, 'CLAUDE.md');
|
||
|
||
if (!existsSync(path)) {
|
||
writeFileSync(path, block + '\n', 'utf8');
|
||
console.log(chalk.green(' ✓ CLAUDE.md(已建立,含 arcrun 區塊)'));
|
||
return;
|
||
}
|
||
const cur = readFileSync(path, 'utf8');
|
||
if (cur.includes(START) && cur.includes(END)) {
|
||
// 取代既有區塊
|
||
const re = new RegExp(escapeRe(START) + '[\\s\\S]*?' + escapeRe(END));
|
||
writeFileSync(path, cur.replace(re, block), 'utf8');
|
||
console.log(chalk.green(' ✓ CLAUDE.md(已更新 arcrun 區塊)'));
|
||
} else {
|
||
writeFileSync(path, cur.replace(/\s*$/, '') + '\n\n' + block + '\n', 'utf8');
|
||
console.log(chalk.green(' ✓ CLAUDE.md(已附加 arcrun 區塊,未動既有內容)'));
|
||
}
|
||
}
|
||
|
||
/** 把 settings.fragment.json 的 hook 合併進專案 settings.json(不覆蓋使用者既有 hooks/設定)。 */
|
||
function mergeSettings(cwd: string, src: string): void {
|
||
const fragment = JSON.parse(readFileSync(join(src, 'settings.fragment.json'), 'utf8'));
|
||
const path = join(cwd, '.claude', 'settings.json');
|
||
mkdirSync(dirname(path), { recursive: true });
|
||
|
||
let settings: Record<string, unknown> = {};
|
||
if (existsSync(path)) {
|
||
try { settings = JSON.parse(readFileSync(path, 'utf8')); } catch { settings = {}; }
|
||
}
|
||
|
||
const hooks = (settings.hooks ?? {}) as Record<string, unknown[]>;
|
||
const fragHooks = fragment.hooks as Record<string, unknown[]>;
|
||
for (const [event, entries] of Object.entries(fragHooks)) {
|
||
const existing = Array.isArray(hooks[event]) ? hooks[event] : [];
|
||
// 去重:避免重裝重複加 arcrun-guard
|
||
const serialized = new Set(existing.map(e => JSON.stringify(e)));
|
||
for (const e of entries) {
|
||
if (!serialized.has(JSON.stringify(e))) existing.push(e);
|
||
}
|
||
hooks[event] = existing;
|
||
}
|
||
settings.hooks = hooks;
|
||
writeFileSync(path, JSON.stringify(settings, null, 2) + '\n', 'utf8');
|
||
}
|
||
|
||
/** 建置期產物的來源片段(`SKILL.md.head` / `.tail`),只給 build-harness-skill.mjs 用,
|
||
* 不該被鋪進使用者專案(使用者拿到的是拼接好的 `SKILL.md`)。 */
|
||
function isBuildSource(name: string): boolean {
|
||
return name.endsWith('.head') || name.endsWith('.tail');
|
||
}
|
||
|
||
/** 遞迴複製目錄樹(覆蓋同名檔;跳過建置期來源片段)。 */
|
||
function copyTree(srcDir: string, dstDir: string): void {
|
||
if (!existsSync(srcDir)) return;
|
||
mkdirSync(dstDir, { recursive: true });
|
||
for (const name of readdirSync(srcDir, { withFileTypes: true })) {
|
||
if (isBuildSource(name.name)) continue;
|
||
const s = join(srcDir, name.name);
|
||
const d = join(dstDir, name.name);
|
||
if (name.isDirectory()) copyTree(s, d);
|
||
else copyFileSync(s, d);
|
||
}
|
||
}
|
||
|
||
function escapeRe(s: string): string {
|
||
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||
}
|