Files
Arcrun/cli/src/commands/install-harness.ts
T
uncle6me-web 46afea83c2 步驟1 最後一筆:install-harness 交付內容升級到現世代+世代閘
管道本來就是好的(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>
2026-07-31 15:29:17 +08:00

135 lines
5.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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.mdappend/取代 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 MCPMCP 對齊中,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, '\\$&');
}