diff --git a/.claude/settings.json b/.claude/settings.json deleted file mode 100644 index e04ac93c..00000000 --- a/.claude/settings.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "hooks": { - "SessionStart": [ - { - "hooks": [ - { "type": "command", "command": "node \"$CLAUDE_PROJECT_DIR/scripts/stack127/agents/models.mjs\" sync --quiet" } - ] - } - ], - "PostToolUse": [ - { - "matcher": "Edit|MultiEdit|Write|NotebookEdit", - "hooks": [ - { "type": "command", "command": "node \"$CLAUDE_PROJECT_DIR/scripts/stack127/agents/lane-note.mjs\" claude" } - ] - } - ] - } -} diff --git a/.codex/config.toml b/.codex/config.toml deleted file mode 100644 index 59339a94..00000000 --- a/.codex/config.toml +++ /dev/null @@ -1,2 +0,0 @@ -model = "gpt-6.1-sol" -model_reasoning_effort = "high" diff --git a/.codex/hooks.json b/.codex/hooks.json deleted file mode 100644 index c53dccf7..00000000 --- a/.codex/hooks.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "hooks": { - "SessionStart": [ - { - "hooks": [ - { "type": "command", "command": "node \"$(git rev-parse --show-toplevel)/scripts/stack127/agents/models.mjs\" sync --quiet" } - ] - } - ], - "PreToolUse": [ - { - "matcher": "apply_patch|Edit|Write", - "hooks": [ - { "type": "command", "command": "node \"$(git rev-parse --show-toplevel)/scripts/stack127/agents/lane-note.mjs\" codex" } - ] - } - ] - } -} diff --git a/AGENTS.md b/AGENTS.md index ce6d1363..21136edc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,7 +12,7 @@ Work is split into three lanes. They are guidance for who usually does what, not | Sol | Newest GPT Sol | `high`, `xhigh` for hard or risky work | Code review, backend changes, and infra planning | | Luna | Newest GPT Luna | `xhigh`, `max` for deep research | Research, mass data gathering, and data work | -Claude Code sessions are the Opus lane. Codex sessions are Sol by default and Luna when started for research or data. Opus usually owns pages, components, styles, card art, and copy. Sol and Luna usually own `lib/`, API routes, scripts, migrations, tests, data, and infra. `scripts/stack127/agents/lanes.mjs` maps files to lanes. A hook adds a short note when you edit a file another lane usually owns. The note never blocks. Use your judgment. +Claude Code sessions are the Opus lane. Codex sessions are Sol by default and Luna when started for research or data. Opus usually owns pages, components, styles, card art, and copy. Sol and Luna usually own `lib/`, API routes, scripts, migrations, tests, data, and infra. These lanes guide task assignment and review. They do not restrict which files an agent may edit when the task requires a cross-lane change. The agent the user talks to is the coordinator: diff --git a/scripts/stack127/agents/lane-note.mjs b/scripts/stack127/agents/lane-note.mjs deleted file mode 100644 index 3504bbfd..00000000 --- a/scripts/stack127/agents/lane-note.mjs +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env node -// Hook for Claude Code and Codex. When a session edits a file another lane -// usually owns, it adds a short note for the agent. It never blocks. -// -// node scripts/stack127/agents/lane-note.mjs claude (Claude Code hook) -// node scripts/stack127/agents/lane-note.mjs codex (Codex hook) - -import fs from "node:fs"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import { laneNames, ownerOf } from "./lanes.mjs"; - -const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../.."); - -// Paths a file-editing tool call touches, relative to the repo. -export function editedFiles(payload) { - const toolName = String(payload?.tool_name ?? payload?.toolName ?? ""); - if (/^(?:Read|Glob|Grep|LS|view_image)$/.test(toolName)) return []; - const input = payload?.tool_input ?? payload?.toolInput ?? {}; - const cwd = path.resolve(repoRoot, payload?.cwd ?? repoRoot); - const files = new Set(); - for (const key of ["file_path", "notebook_path", "target_file", "path"]) { - if (typeof input[key] === "string") files.add(path.resolve(cwd, input[key])); - } - // Codex apply_patch carries file names inside the patch text. - for (const text of [input.command, input.patch, input.input]) { - if (typeof text !== "string") continue; - for (const match of text.matchAll(/^\*\*\* (?:Add|Update|Delete) File: (.+)$|^\*\*\* Move to: (.+)$/gm)) { - files.add(path.resolve(cwd, (match[1] ?? match[2]).trim())); - } - } - return [...files] - .map((file) => path.relative(repoRoot, file)) - .filter((rel) => rel && !rel.startsWith("..") && !path.isAbsolute(rel)) - .map((rel) => rel.split(path.sep).join("/")); -} - -export function laneNote(payload, tool = "claude") { - const other = tool === "codex" ? "opus" : "codex"; - const files = editedFiles(payload).filter((file) => ownerOf(file) === other); - if (!files.length) return null; - return `Lane note: ${files.join(", ")} is usually ${laneNames[other]} work. That's fine if it's part of your task. If not, consider handing it to that lane. See "Agent roles" in AGENTS.md.`; -} - -if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { - try { - const payload = JSON.parse(fs.readFileSync(0, "utf8")); - const note = laneNote(payload, process.argv[2]); - if (note) { - console.log(JSON.stringify({ - systemMessage: note, - hookSpecificOutput: { hookEventName: payload.hook_event_name ?? "PreToolUse", additionalContext: note }, - })); - } - } catch { - // A note is optional. Never block a tool call over it. - } - process.exit(0); -} diff --git a/scripts/stack127/agents/lanes.mjs b/scripts/stack127/agents/lanes.mjs deleted file mode 100644 index 2924e805..00000000 --- a/scripts/stack127/agents/lanes.mjs +++ /dev/null @@ -1,53 +0,0 @@ -// Which lane usually owns which files. The lane note hook and AGENTS.md both follow this. -// Opus: writing, design, frontend, site content, and planning. -// Codex (Sol and Luna): backend, data, infra, and tests. - -import path from "node:path"; - -// Exceptions must precede the broad lib/ rule. -const opusExceptions = [/^lib\/card-art\.ts$/]; - -const opusPatterns = [ - /^app\/(?!api\/).*/, - /^public\/(?:brand|course-images|logos)\//, - /^public\/(?:favicon[^/]*|apple-touch-icon\.png|android-chrome-[^/]+\.png)$/, - /^content_docs\/[^/]+\/(tool-spec|brief|writing-and-seo|interface-contract)\.md$/, - /^stack127_docs\/(writing-style|shared-ui|design-system|css-architecture|tool-interface|page-families)\.md$/, -]; - -const codexPatterns = [ - /^app\/api\//, - /^app\/(.+\/)?route\.(ts|tsx|js|mjs)$/, - /^app\/(sitemap|robots|manifest)\.[a-z]+$/, - /^lib\//, - /^scripts\//, - /^supabase\//, - /^migrations\//, - /^tests\//, - /^\.github\//, - /^\.(?:codex|claude)\/(?:config\.toml|hooks\.json|settings(?:\.local)?\.json)$/, - /^\.env/, - /^(?:proxy|middleware)\.[cm]?[jt]sx?$/, - /^(package|package-lock)\.json$/, - /^(next\.config|tsconfig)[^/]*$/, - /^(Dockerfile|docker-compose)[^/]*$/, - /^content_docs\/[^/]+\/(?:research(?:\/|\.md$)|competitive-report\.md$)/, - /^content_docs\/.*\.(?:json|csv|tsv|sql)$/, - /^stack127_docs\/(production-deployment|homelab-development|caching|tool-pages|quality-workflow|seo)\.md$/, -]; - -// Codex rules win over the broad app/ rule, so route handlers and SEO runtime files stay with Codex. -export function ownerOf(relativePath) { - const normalized = path.posix.normalize(relativePath.replaceAll("\\", "/")); - // Starter repositories follow the same ownership as their eventual files. - if (normalized.startsWith("templates/stack127-tool-repo/")) return ownerOf(normalized.slice("templates/stack127-tool-repo/".length)); - if (opusExceptions.some((pattern) => pattern.test(normalized))) return "opus"; - if (codexPatterns.some((pattern) => pattern.test(normalized))) return "codex"; - if (opusPatterns.some((pattern) => pattern.test(normalized))) return "opus"; - return "shared"; -} - -export const laneNames = { - opus: "Opus (writing, design, frontend, site content, planning)", - codex: "Sol or Luna (backend, data, infra, tests)", -}; diff --git a/scripts/stack127/agents/models.mjs b/scripts/stack127/agents/models.mjs deleted file mode 100644 index c9a3c093..00000000 --- a/scripts/stack127/agents/models.mjs +++ /dev/null @@ -1,78 +0,0 @@ -#!/usr/bin/env node -// Picks the newest Sol and Luna models from Codex's model list, and keeps the -// project Codex default on the newest Sol. -// -// node scripts/stack127/agents/models.mjs print the current picks -// node scripts/stack127/agents/models.mjs sync update .codex/config.toml - -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../.."); -const codexHome = process.env.CODEX_HOME || path.join(os.homedir(), ".codex"); -const projectConfig = path.join(repoRoot, ".codex", "config.toml"); - -function versionOf(slug, family) { - const match = slug.match(new RegExp(`^gpt-(\\d+(?:\\.\\d+)*)-${family}$`)); - return match ? match[1].split(".").map(Number) : null; -} - -function compareVersions(a, b) { - for (let i = 0; i < Math.max(a.length, b.length); i += 1) { - const diff = (a[i] ?? 0) - (b[i] ?? 0); - if (diff !== 0) return diff; - } - return 0; -} - -export function newestModel(family, { cachePath = path.join(codexHome, "models_cache.json") } = {}) { - if (!["sol", "luna"].includes(family)) return null; - let models = []; - try { - const cache = JSON.parse(fs.readFileSync(cachePath, "utf8")); - models = Array.isArray(cache) ? cache : cache.models ?? cache.data ?? []; - } catch { - return null; - } - if (!Array.isArray(models)) return null; - let best = null; - for (const model of models) { - if (!model || typeof model !== "object" || typeof model.slug !== "string") continue; - if (model.visibility && model.visibility !== "list") continue; - const version = versionOf(model.slug ?? "", family); - if (version && (!best || compareVersions(version, best.version) > 0)) best = { slug: model.slug, version }; - } - return best?.slug ?? null; -} - -export function syncProjectConfig({ configPath = projectConfig, cachePath } = {}) { - const sol = newestModel("sol", { cachePath }); - if (!sol) return { changed: false, model: null }; - const current = fs.existsSync(configPath) ? fs.readFileSync(configPath, "utf8") : ""; - const sectionStart = current.search(/^\s*\[/m); - let root = sectionStart < 0 ? current : current.slice(0, sectionStart); - const sections = sectionStart < 0 ? "" : current.slice(sectionStart); - for (const [key, value] of [["model", sol], ["model_reasoning_effort", "high"]]) { - const pattern = new RegExp(`^[ \\t]*${key}\\s*=.*$`, "m"); - const line = `${key} = "${value}"`; - root = pattern.test(root) ? root.replace(pattern, line) : `${line}\n${root}`; - } - const next = root + sections; - if (next === current) return { changed: false, model: sol }; - fs.mkdirSync(path.dirname(configPath), { recursive: true }); - fs.writeFileSync(configPath, next); - return { changed: true, model: sol }; -} - -if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { - if (process.argv[2] === "sync") { - const { changed, model } = syncProjectConfig(); - if (process.argv[3] !== "--quiet") console.log(model ? `Codex default: ${model}${changed ? " (updated)" : ""}` : "Codex model list not found."); - } else { - console.log(`sol: ${newestModel("sol") ?? "unknown"}`); - console.log(`luna: ${newestModel("luna") ?? "unknown"}`); - console.log("opus: opus (Claude Code alias for the newest Opus)"); - } -} diff --git a/scripts/stack127/setup-worktree.mjs b/scripts/stack127/setup-worktree.mjs index 1958e356..b6f1d9b7 100644 --- a/scripts/stack127/setup-worktree.mjs +++ b/scripts/stack127/setup-worktree.mjs @@ -88,7 +88,7 @@ export async function setupWorktree() { await run(root, "npm", ["ci", "--include=dev", "--no-audit", "--no-fund"]); console.log("Generating this checkout's Next.js types..."); await run(root, process.execPath, ["node_modules/next/dist/bin/next", "typegen"]); - for (const name of [".agents/skills", ".claude/skills", ".codex/hooks.json", ".claude/settings.json"]) { + for (const name of [".agents/skills", ".claude/skills"]) { await access(path.join(root, name)); } console.log("Worktree ready. Use Dev and run or Build and run for a preview."); diff --git a/tests/stack127/setup-worktree.test.mjs b/tests/stack127/setup-worktree.test.mjs index 7ff8c97b..80079352 100644 --- a/tests/stack127/setup-worktree.test.mjs +++ b/tests/stack127/setup-worktree.test.mjs @@ -17,11 +17,10 @@ async function fixture(t) { const bin = path.join(directory, "bin"); await mkdir(main); await mkdir(bin); - for (const folder of ["skills", ".agents", ".claude/skills", ".codex"]) await mkdir(path.join(main, folder), { recursive: true }); + for (const folder of ["skills", ".agents", ".claude/skills"]) await mkdir(path.join(main, folder), { recursive: true }); await symlink("../skills", path.join(main, ".agents/skills")); await writeFile(path.join(main, "skills/example.txt"), "skill fixture"); await writeFile(path.join(main, ".claude/skills/example.txt"), "skill fixture"); - for (const file of [".codex/hooks.json", ".claude/settings.json"]) await writeFile(path.join(main, file), "{}"); await writeFile(path.join(main, "package.json"), JSON.stringify({ name: "stack127" })); await writeFile(path.join(main, "package-lock.json"), "{}"); await writeFile(path.join(main, ".gitignore"), ".env*\nnode_modules/\n.next/\ntmp/\n");