🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

spec-first

Package Overview
Dependencies
Maintainers
1
Versions
89
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

spec-first - npm Package Compare versions

Comparing version
1.13.0
to
1.13.1
+3
-0
docs/contracts/release-package-evidence.schema.json

@@ -46,2 +46,3 @@ {

"init_codex_programmatic_log",
"init_codex_cli_log",
"init_cursor_programmatic_log",

@@ -61,2 +62,3 @@ "init_kiro_programmatic_log",

"init_codex_programmatic_log": { "type": "string", "const": "init-codex-programmatic.log" },
"init_codex_cli_log": { "type": "string", "const": "init-codex-cli.log" },
"init_cursor_programmatic_log": { "type": "string", "const": "init-cursor-programmatic.log" },

@@ -105,2 +107,3 @@ "init_kiro_programmatic_log": { "type": "string", "const": "init-kiro-programmatic.log" },

{ "properties": { "checks": { "contains": { "type": "object", "required": ["check_id"], "properties": { "check_id": { "const": "init-codex-programmatic" } } } } } },
{ "properties": { "checks": { "contains": { "type": "object", "required": ["check_id"], "properties": { "check_id": { "const": "init-codex-cli" } } } } } },
{ "properties": { "checks": { "contains": { "type": "object", "required": ["check_id"], "properties": { "check_id": { "const": "init-cursor-programmatic" } } } } } },

@@ -107,0 +110,0 @@ { "properties": { "checks": { "contains": { "type": "object", "required": ["check_id"], "properties": { "check_id": { "const": "init-kiro-programmatic" } } } } } },

+1
-1
{
"name": "spec-first",
"version": "1.13.0",
"version": "1.13.1",
"description": "AI Coding Harness for Claude Code, Codex, Kiro, Qoder, and Cursor generated-runtime preview — turns one-off AI coding chats into a repo-backed, verifiable engineering loop for spec-driven development. Scripts prepare facts; LLMs decide; evidence stays in your repo.",

@@ -5,0 +5,0 @@ "type": "commonjs",

@@ -80,3 +80,3 @@ <div align="center">

Select your host (Claude Code, Codex, Cursor, Kiro, and/or Qoder), confirm your developer name and language, then confirm the writes. Scripted preview setup uses `spec-first init --kiro -y -u <name> --lang <zh|en>` for Kiro, `spec-first init --qoder -y -u <name> --lang <zh|en>` for Qoder, or `spec-first init --cursor -y -u <name> --lang <zh|en>` for Cursor generated-runtime preview. Cursor is not part of the `init -y` default host set.
Select your host (Claude Code, Codex, Cursor, Kiro, and/or Qoder), confirm your developer name and language, then confirm the writes. In a parent workspace with many child Git repos, `init` defaults to writing only the parent workspace runtime; use `--repo <child>` only when one child repo should be an independent agent root, and reserve `--all-repos` for explicit batch maintenance. Scripted `init -y` setup on fresh machines must pass `-u <name>` because there is no prompt to collect a developer name, for example `spec-first init --codex -y -u <name> --lang <zh|en>`. Scripted preview setup uses `spec-first init --kiro -y -u <name> --lang <zh|en>` for Kiro, `spec-first init --qoder -y -u <name> --lang <zh|en>` for Qoder, or `spec-first init --cursor -y -u <name> --lang <zh|en>` for Cursor generated-runtime preview. Cursor is not part of the `init -y` default host set.

@@ -83,0 +83,0 @@ Expected: init lists the generated runtime paths under `.claude/`, `.codex/`, `.agents/skills/`, `.cursor/`, `.kiro/`, or `.qoder/`. Generated copies can be rebuilt any time with `spec-first init`.

@@ -80,3 +80,3 @@ <div align="center">

选择宿主(Claude Code、Codex、Cursor、Kiro 和/或 Qoder)、确认开发者姓名与语言,然后确认写入。脚本化 preview 初始化可使用 `spec-first init --kiro -y -u <name> --lang <zh|en>` 初始化 Kiro,使用 `spec-first init --qoder -y -u <name> --lang <zh|en>` 初始化 Qoder,或使用 `spec-first init --cursor -y -u <name> --lang <zh|en>` 初始化 Cursor generated-runtime preview。Cursor 不属于 `init -y` 默认宿主集合。
选择宿主(Claude Code、Codex、Cursor、Kiro 和/或 Qoder)、确认开发者姓名与语言,然后确认写入。在包含大量 child Git repos 的父级 workspace 中,`init` 默认只写父级 workspace runtime;只有某个 child repo 需要作为独立 agent root 时才使用 `--repo <child>`,`--all-repos` 仅保留给显式批量维护。全新机器上的脚本化 `init -y` 必须传入 `-u <name>`,因为非交互模式不会再提示输入开发者姓名,例如 `spec-first init --codex -y -u <name> --lang <zh|en>`。脚本化 preview 初始化可使用 `spec-first init --kiro -y -u <name> --lang <zh|en>` 初始化 Kiro,使用 `spec-first init --qoder -y -u <name> --lang <zh|en>` 初始化 Qoder,或使用 `spec-first init --cursor -y -u <name> --lang <zh|en>` 初始化 Cursor generated-runtime preview。Cursor 不属于 `init -y` 默认宿主集合。

@@ -83,0 +83,0 @@ 预期结果:init 列出 `.claude/`、`.codex/`、`.agents/skills/`、`.cursor/`、`.kiro/` 或 `.qoder/` 下的生成路径,并确认 setup 完成。生成的 runtime copies 随时可通过 `spec-first init` 重建。

@@ -12,2 +12,3 @@ #!/usr/bin/env node

const INIT_CODEX_PROGRAMMATIC_LOG_FILE = 'init-codex-programmatic.log';
const INIT_CODEX_CLI_LOG_FILE = 'init-codex-cli.log';
const INIT_CURSOR_PROGRAMMATIC_LOG_FILE = 'init-cursor-programmatic.log';

@@ -145,2 +146,3 @@ const INIT_KIRO_PROGRAMMATIC_LOG_FILE = 'init-kiro-programmatic.log';

windowsHide: true,
windowsVerbatimArguments: options.windowsVerbatimArguments,
});

@@ -179,5 +181,15 @@ }

stdio: options.stdio || 'inherit',
windowsVerbatimArguments: true,
});
}
function runWindowsCmdShimResult(shim, args, options = {}) {
const comspec = getEnvValue(options.env || process.env, 'ComSpec') || 'cmd.exe';
return runChildResult(comspec, ['/d', '/c', buildCmdCommandLine(shim, args)], {
...options,
stdio: options.stdio || 'pipe',
windowsVerbatimArguments: true,
});
}
function runInstalledShim(shim, args, options = {}) {

@@ -193,2 +205,9 @@ if (process.platform === 'win32') {

function runInstalledShimResult(shim, args, options = {}) {
if (process.platform === 'win32') {
return runWindowsCmdShimResult(shim, args, options);
}
return runChildResult(shim, args, options);
}
function runInstalledBin(packageRoot, args, options = {}) {

@@ -402,2 +421,15 @@ const binPath = path.join(packageRoot, 'bin', 'spec-first.js');

function buildInitCliEvidence({ host, result, beforeSnapshot, afterSnapshot }) {
const evidence = buildInitProgrammaticEvidence({
host,
result,
beforeSnapshot,
afterSnapshot,
});
return {
...evidence,
reason_code: evidence.passed ? 'init-cli-passed' : 'init-cli-failed',
};
}
function runInstalledProgrammaticInitResult({ packageRoot, cwd, host, name = 'matrix', lang = 'en' }) {

@@ -440,2 +472,10 @@ const source = `

function runInstalledCliInitResult({ packageRoot, shim = '', cwd, host, name = 'matrix', lang = 'en' }) {
const args = ['init', `--${host}`, '-y', '-u', name, '--lang', lang];
if (shim) {
return runInstalledShimResult(shim, args, { cwd });
}
return runInstalledBinResult(packageRoot, args, { cwd });
}
function runInitProgrammaticEvidence({ packageRoot, cwd, host, artifacts }) {

@@ -477,2 +517,39 @@ const logName = INIT_PROGRAMMATIC_LOG_FILES[host] || `init-${host}-programmatic.log`;

function runInitCliEvidence({ packageRoot, shim = '', cwd, host, artifacts }) {
const logName = host === 'codex' ? INIT_CODEX_CLI_LOG_FILE : `init-${host}-cli.log`;
const beforeSnapshot = snapshotTree(cwd);
const result = runInstalledCliInitResult({ packageRoot, shim, cwd, host });
const afterSnapshot = snapshotTree(cwd);
const evidence = buildInitCliEvidence({
host,
result,
beforeSnapshot,
afterSnapshot,
});
artifacts.write(logName, [
`host=${host}`,
`command=spec-first init --${host} -y -u <name> --lang <lang>`,
`status=${evidence.status}`,
`passed=${evidence.passed}`,
`reason_code=${evidence.reason_code}`,
`mutated=${evidence.mutated}`,
`expected_state_path=${evidence.expected_state_path}`,
`has_state=${evidence.has_state}`,
`expected_instruction_path=${evidence.expected_instruction_path}`,
`has_instruction=${evidence.has_instruction}`,
`expected_runtime_paths=${evidence.expected_runtime_paths.join(',')}`,
`present_runtime_paths=${evidence.present_runtime_paths.join(',')}`,
`missing_runtime_paths=${evidence.missing_runtime_paths.join(',')}`,
'',
'--- stdout ---',
evidence.stdout,
'--- stderr ---',
evidence.stderr,
].join('\n'));
return {
...evidence,
artifact_path: logName,
};
}
function readPackageInfo(cwd) {

@@ -493,2 +570,3 @@ const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));

init_codex_programmatic_log: INIT_CODEX_PROGRAMMATIC_LOG_FILE,
init_codex_cli_log: INIT_CODEX_CLI_LOG_FILE,
init_cursor_programmatic_log: INIT_CURSOR_PROGRAMMATIC_LOG_FILE,

@@ -528,2 +606,14 @@ init_kiro_programmatic_log: INIT_KIRO_PROGRAMMATIC_LOG_FILE,

function checkFromInitCliEvidence(evidence) {
return {
check_id: `init-${evidence.host}-cli`,
status: evidence.passed ? 'passed' : 'failed',
reason_code: evidence.reason_code,
summary: evidence.passed
? `CLI spec-first init -y for ${evidence.host} passed and wrote expected runtime evidence.`
: `CLI spec-first init -y for ${evidence.host} failed evidence checks.`,
artifact_path: evidence.artifact_path,
};
}
function buildCursorLoaderEvidence(options = {}) {

@@ -805,2 +895,17 @@ return {

const cliCodexProject = path.join(tmp, 'cli-codex workspace [win64] 中文');
fs.mkdirSync(cliCodexProject);
const cliCodexEvidence = runInitCliEvidence({
packageRoot,
shim,
cwd: cliCodexProject,
host: 'codex',
artifacts,
});
const cliCodexCheck = checkFromInitCliEvidence(cliCodexEvidence);
releaseChecks.push(cliCodexCheck);
if (cliCodexCheck.status === 'failed') {
throw new Error('CLI Codex init evidence check failed.');
}
const cursorLoaderEvidence = buildCursorLoaderEvidence({

@@ -917,2 +1022,5 @@ generatedAt: new Date().toISOString(),

},
init_cli_artifacts: {
codex: INIT_CODEX_CLI_LOG_FILE,
},
cursor_doctor_programmatic: CURSOR_DOCTOR_PROGRAMMATIC_LOG_FILE,

@@ -957,2 +1065,5 @@ cursor_clean_programmatic: CURSOR_CLEAN_PROGRAMMATIC_LOG_FILE,

},
init_cli_artifacts: {
codex: INIT_CODEX_CLI_LOG_FILE,
},
cursor_doctor_programmatic: CURSOR_DOCTOR_PROGRAMMATIC_LOG_FILE,

@@ -975,2 +1086,3 @@ cursor_clean_programmatic: CURSOR_CLEAN_PROGRAMMATIC_LOG_FILE,

module.exports = {
buildInitCliEvidence,
buildInitProgrammaticEvidence,

@@ -982,2 +1094,3 @@ buildPackageContentManifest,

checkFromCursorLoaderEvidence,
checkFromInitCliEvidence,
checkFromInitProgrammaticEvidence,

@@ -992,2 +1105,3 @@ checkFromPackageContentManifest,

INIT_CODEX_PROGRAMMATIC_LOG_FILE,
INIT_CODEX_CLI_LOG_FILE,
INIT_CURSOR_PROGRAMMATIC_LOG_FILE,

@@ -1015,8 +1129,12 @@ INIT_KIRO_PROGRAMMATIC_LOG_FILE,

runGit,
runInitCliEvidence,
runInstalledBin,
runInstalledBinResult,
runInstalledCliInitResult,
runInstalledShim,
runInstalledShimResult,
runNpm,
runWindowsCmdShim,
runWindowsCmdShimResult,
SUMMARY_FILE,
};

@@ -185,3 +185,4 @@ param(

[string]$HostName,
[string]$TargetRoot
[string]$TargetRoot,
[string]$NextAction = 'spec-first init -y -u <name>'
)

@@ -194,3 +195,3 @@

$reasonCode = 'unknown-runtime-manifest-health'
$nextAction = 'spec-first init -y'
$nextAction = $NextAction

@@ -238,2 +239,13 @@ if ([string]::IsNullOrWhiteSpace($HostName) -or [string]::IsNullOrWhiteSpace($TargetRoot) -or [string]::IsNullOrWhiteSpace($statePath)) {

function Get-GeneratedRuntimeRefreshActionForFacts {
param([object]$Facts)
$selectionSource = [string](Get-NestedValue -InputObject $Facts -PathParts @('target', 'selection_source'))
$repoLabel = [string](Get-NestedValue -InputObject $Facts -PathParts @('target', 'repo_label'))
if ($selectionSource -eq 'explicit-repo' -and -not [string]::IsNullOrWhiteSpace($repoLabel) -and $repoLabel -ne '.') {
return "spec-first init --repo $repoLabel -y -u <name>"
}
return 'spec-first init -y -u <name>'
}
function Test-PathIsSymlink {

@@ -535,10 +547,28 @@ param([string]$Path)

$childReason = if ($childManifestRefreshRequired) { 'generated-runtime-manifest-refresh-required' } elseif ($childStatus -ne 0 -and [string]::IsNullOrWhiteSpace([string]$childLedger.reason_code)) { 'child-verify-failed' } else { [string]$childLedger.reason_code }
$childGeneratedRuntimeManifest = if ($childLedger.PSObject.Properties.Name -contains 'generated_runtime_manifest') { $childLedger.generated_runtime_manifest } else { [pscustomobject]@{ status = 'unknown'; reason_code = 'not-reported' } }
$childNextActions = @($childLedger.next_actions)
if ($childManifestRefreshRequired) {
$childManifestAction = "spec-first init --repo $($child.workspace_relative_path) -y -u <name>"
if ($childGeneratedRuntimeManifest -is [System.Collections.IDictionary]) {
$childGeneratedRuntimeManifest['next_action'] = $childManifestAction
} elseif ($childGeneratedRuntimeManifest.PSObject.Properties.Name -contains 'next_action') {
$childGeneratedRuntimeManifest.next_action = $childManifestAction
} else {
$childGeneratedRuntimeManifest | Add-Member -NotePropertyName 'next_action' -NotePropertyValue $childManifestAction
}
$childNextActions = @($childNextActions | ForEach-Object {
if ($_ -eq 'spec-first init -y' -or $_ -eq 'spec-first init -y -u <name>') { $childManifestAction } else { $_ }
})
if (-not ($childNextActions -contains $childManifestAction)) {
$childNextActions += $childManifestAction
}
}
$childResult = [pscustomobject]@{
schema_version = 'mcp-verify-child-result.v1'
baseline_ready = [bool]$childLedger.baseline_ready
generated_runtime_manifest = if ($childLedger.PSObject.Properties.Name -contains 'generated_runtime_manifest') { $childLedger.generated_runtime_manifest } else { [pscustomobject]@{ status = 'unknown'; reason_code = 'not-reported' } }
generated_runtime_manifest = $childGeneratedRuntimeManifest
tool_facts_status = $childLedger.tool_facts_status
runtime_capabilities_status = $childLedger.runtime_capabilities_status
reason_code = [string]$childLedger.reason_code
next_actions = @($childLedger.next_actions)
next_actions = @($childNextActions)
}

@@ -578,3 +608,3 @@ } catch {

if (@('stale', 'missing') -contains [string]$parentGeneratedRuntimeManifest.status) {
$parentGeneratedRuntimeManifest['next_action'] = 'spec-first init --all-repos -y'
$parentGeneratedRuntimeManifest['next_action'] = 'spec-first init -y -u <name>'
}

@@ -635,6 +665,6 @@ $readyCount = @($results | Where-Object { $_.overall_status -eq 'ready' }).Count

if ($manifestRefreshRequiredCount -gt 0) {
'- Generated runtime manifest stale or missing in the parent workspace or one or more child repos. Run `spec-first init --all-repos -y` from the parent workspace.'
'- Generated runtime manifest stale or missing in the parent workspace or one or more child repos. Run `spec-first init -y -u <name>` for the parent workspace runtime; use `spec-first init --repo <child> -y -u <name>` for a stale child repo, or explicit `spec-first init --all-repos -y -u <name>` for intentional batch child-root refresh.'
}
)
next_action = if ($manifestRefreshRequiredCount -gt 0) { 'Run spec-first init --all-repos -y from the parent workspace, then rerun verify.' } elseif ($actionRequiredCount -eq 0) { 'All child repos verified required MCP/helper dependency readiness.' } else { 'Inspect per-child reason_code and rerun setup/verify for action-required repos.' }
next_action = if ($manifestRefreshRequiredCount -gt 0) { 'Run spec-first init -y -u <name> from the parent workspace for parent runtime, or spec-first init --repo <child> -y -u <name> for stale child repos, then rerun verify.' } elseif ($actionRequiredCount -eq 0) { 'All child repos verified required MCP/helper dependency readiness.' } else { 'Inspect per-child reason_code and rerun setup/verify for action-required repos.' }
}

@@ -708,3 +738,4 @@

$HostPointerReconciliation = Get-HostPointerReconciliation -CurrentHost $reconciliationHost -RepoRoot $reconciliationRepoRoot -MarkerPathArg $MarkerPath
$GeneratedRuntimeManifest = Get-GeneratedRuntimeManifestHealth -HostName $reconciliationHost -TargetRoot $reconciliationRepoRoot
$generatedRuntimeRefreshAction = Get-GeneratedRuntimeRefreshActionForFacts -Facts $Facts
$GeneratedRuntimeManifest = Get-GeneratedRuntimeManifestHealth -HostName $reconciliationHost -TargetRoot $reconciliationRepoRoot -NextAction $generatedRuntimeRefreshAction

@@ -711,0 +742,0 @@ function Test-ToolReady {

@@ -146,2 +146,3 @@ #!/bin/bash

local target_root="$2"
local refresh_action="${3:-spec-first init -y -u <name>}"
local bundled_version state_path recorded_version status reason next_action

@@ -154,3 +155,3 @@

reason="unknown-runtime-manifest-health"
next_action="spec-first init -y"
next_action="$refresh_action"

@@ -203,2 +204,15 @@ if [ -z "$host" ] || [ -z "$target_root" ] || [ -z "$state_path" ]; then

generated_runtime_refresh_action_for_facts() {
local facts_json="$1"
jq -r '
(.target.selection_source // "") as $selection_source
| (.target.repo_label // "") as $repo_label
| if ($selection_source == "explicit-repo" and $repo_label != "" and $repo_label != ".") then
"spec-first init --repo \($repo_label) -y -u <name>"
else
"spec-first init -y -u <name>"
end
' <<<"$facts_json"
}
write_workspace_summary_atomic() {

@@ -504,10 +518,21 @@ local workspace_root="$1"

' <<<"$child_ledger")"
child_result="$(jq -n --argjson ledger "$child_ledger" '{
child_manifest_action="spec-first init --repo $child_path -y -u <name>"
child_result="$(jq -n --argjson ledger "$child_ledger" --arg child_manifest_action "$child_manifest_action" '
($ledger.generated_runtime_manifest // {status:"unknown", reason_code:"not-reported"}) as $manifest
| ((($manifest.status // "unknown") == "stale") or (($manifest.status // "unknown") == "missing")) as $manifest_refresh_required
| ($ledger.next_actions // []) as $actions
| {
schema_version:"mcp-verify-child-result.v1",
baseline_ready:($ledger.baseline_ready // false),
generated_runtime_manifest:($ledger.generated_runtime_manifest // {status:"unknown", reason_code:"not-reported"}),
generated_runtime_manifest:(if $manifest_refresh_required then ($manifest | .next_action = $child_manifest_action) else $manifest end),
tool_facts_status:($ledger.tool_facts_status // "unknown"),
runtime_capabilities_status:($ledger.runtime_capabilities_status // "unknown"),
reason_code:($ledger.reason_code // ""),
next_actions:($ledger.next_actions // [])
next_actions:(
if $manifest_refresh_required then
(($actions | map(if . == "spec-first init -y" or . == "spec-first init -y -u <name>" then $child_manifest_action else . end)) + [$child_manifest_action] | unique)
else
$actions
end
)
}')"

@@ -549,9 +574,9 @@ else

parent_host="$(jq -r '.host // empty' <<<"$HOST_INFO_JSON")"
parent_generated_runtime_manifest="$(compute_generated_runtime_manifest_health "$parent_host" "$workspace_root" | jq '
if ((.status // "unknown") == "stale") or ((.status // "unknown") == "missing") then
.next_action = "spec-first init --all-repos -y"
else
.
end
')"
parent_generated_runtime_manifest="$(compute_generated_runtime_manifest_health "$parent_host" "$workspace_root" | jq '
if ((.status // "unknown") == "stale") or ((.status // "unknown") == "missing") then
.next_action = "spec-first init -y -u <name>"
else
.
end
')"

@@ -620,10 +645,10 @@ summary_json="$(jq -n \

else [] end)
+ (if ($parent_manifest_refresh_required or ($child_manifest_refresh_required_count > 0)) then
["- Generated runtime manifest stale or missing in the parent workspace or one or more child repos. Run `spec-first init --all-repos -y` from the parent workspace."]
else [] end)
),
next_action:(
if ($parent_manifest_refresh_required or ($child_manifest_refresh_required_count > 0)) then
"Run spec-first init --all-repos -y from the parent workspace, then rerun verify."
elif ([$results[] | select(.overall_status != "ready")] | length) == 0 then
+ (if ($parent_manifest_refresh_required or ($child_manifest_refresh_required_count > 0)) then
["- Generated runtime manifest stale or missing in the parent workspace or one or more child repos. Run `spec-first init -y -u <name>` for the parent workspace runtime; use `spec-first init --repo <child> -y -u <name>` for a stale child repo, or explicit `spec-first init --all-repos -y -u <name>` for intentional batch child-root refresh."]
else [] end)
),
next_action:(
if ($parent_manifest_refresh_required or ($child_manifest_refresh_required_count > 0)) then
"Run spec-first init -y -u <name> from the parent workspace for parent runtime, or spec-first init --repo <child> -y -u <name> for stale child repos, then rerun verify."
elif ([$results[] | select(.overall_status != "ready")] | length) == 0 then
"All child repos verified required MCP/helper dependency readiness."

@@ -701,3 +726,4 @@ else

HOST_POINTER_RECONCILIATION="$(compute_host_pointer_reconciliation "$RECONCILIATION_HOST" "$RECONCILIATION_REPO_ROOT" "$MARKER_PATH")"
GENERATED_RUNTIME_MANIFEST="$(compute_generated_runtime_manifest_health "$RECONCILIATION_HOST" "$RECONCILIATION_REPO_ROOT")"
GENERATED_RUNTIME_REFRESH_ACTION="$(generated_runtime_refresh_action_for_facts "$FACTS_JSON")"
GENERATED_RUNTIME_MANIFEST="$(compute_generated_runtime_manifest_health "$RECONCILIATION_HOST" "$RECONCILIATION_REPO_ROOT" "$GENERATED_RUNTIME_REFRESH_ACTION")"
HELPER_JSON="$(

@@ -704,0 +730,0 @@ SPEC_FIRST_PROVIDER_HOST="$RECONCILIATION_HOST" \

@@ -23,3 +23,3 @@ ---

Core boundary: scripts prepare deterministic readiness facts; LLM workflows decide how to use those facts. Setup must not make semantic code-understanding judgments or require any external analysis service before ordinary work can continue.
Core boundary: scripts prepare deterministic readiness facts; LLM workflows decide how to use those facts. Setup must not make semantic code-understanding judgments or require any external analysis service before ordinary work can continue. After CodeGraph/Graphify readiness is prepared, setup may recommend `spec-rule-miner` as a next step for evidence-backed project AI coding rules; it must not invoke rule mining, synthesize rules, or write `docs/ai/project-rules.md`.

@@ -101,5 +101,5 @@ ## Scenario Capability

- `.gitignore` policy: `spec-first init`'s managed block ignores `.codegraph/` and the whole `graphify-out/` provider artifact directory; setup does not auto-add, auto-commit, or promote Graphify output to source truth;
- non-actions: no `graphify watch`/long-running daemon, no optional Graphify MCP server install, no generated artifact promotion to `docs/` or source truth.
- non-actions: no `graphify watch`/long-running daemon, no optional Graphify MCP server install, no generated artifact promotion to `docs/` or source truth, no automatic `spec-rule-miner` invocation, and no project-rule file writes.
3. If the plan reports an unknown provider selection or unresolved target, stop with the blocked reason and next action instead of installing.
4. Otherwise, run the internal apply path with `--only codegraph,graphify` plus any `--repo`/`--folder`/`--requirement-workspace` args already supplied, then run verification/fact refresh and render the final grouped status block.
4. Otherwise, run the internal apply path with `--only codegraph,graphify` plus any `--repo`/`--folder`/`--requirement-workspace` args already supplied, then run verification/fact refresh and render the final grouped status block. If CodeGraph/Graphify readiness is ready or degraded-but-usable and no setup blocker remains, the final next step should suggest running `spec-rule-miner` separately to mine project rules from source evidence.

@@ -113,3 +113,3 @@ ## Workflow

5. Run `install-helpers.*` for required helper tooling and explicitly approved non-MCP provider helpers. Bare setup or `--only graphify` is enough public consent for Graphify; the script layer may translate that to helper env consent internally. Approved provider first generation and project-local auto-refresh setup may run only through controlled script cases or provider-native bounded CLI commands invoked through the resolved CLI path, then helper/provider readiness facts are collected. If `graphify extract .` fails in the default project-root scope, the script should try code-only `graphify update .` before returning failed readiness. If Graphify is installed but not visible on the user's original `PATH`, report the manual visibility action instead of editing shell profiles; for provider-owned git hooks, setup may repair only the project-local Graphify hook PATH block so hook verification can succeed without global profile edits. If Graphify hook install still fails after bounded repair, report `readiness_status=degraded` with `next_actions` instead of marking hook refresh verified.
6. Run `verify-tools.*` to write the readiness ledger, reconcile host pointer facts, write project setup facts, and render the grouped status block. Read `generated_runtime_manifest.status` separately from `baseline_ready`; `baseline_ready=true` must not hide stale generated runtime. If the status is `stale` or `missing`, refresh runtime with the topology-appropriate command (`spec-first init -y` for the current repo, `spec-first init --all-repos -y` from a parent workspace, or `spec-first init --repo <child> -y` for one child repo), then rerun verification. If `spec-first update` just ran and the status is still stale, treat that as degraded refresh evidence and surface the same fallback command instead of reporting runtime freshness as ready.
6. Run `verify-tools.*` to write the readiness ledger, reconcile host pointer facts, write project setup facts, and render the grouped status block. Read `generated_runtime_manifest.status` separately from `baseline_ready`; `baseline_ready=true` must not hide stale generated runtime. If the status is `stale` or `missing`, refresh runtime with the topology-appropriate command (`spec-first init -y` for the current repo or parent workspace runtime, `spec-first init --repo <child> -y` for one child repo, or explicit `spec-first init --all-repos -y` only for intentional batch child-root refresh), then rerun verification. If `spec-first update` just ran and the status is still stale, treat that as degraded refresh evidence and surface the same fallback command instead of reporting runtime freshness as ready.
7. Report the status exactly enough for the user to act: ready rows need no action; action-required rows name the missing dependency/config/target step; generated runtime manifest rows name the init refresh command when stale or missing.

@@ -129,3 +129,3 @@

- `Verification profile`: current verification profile visibility placeholder; full profile execution is v1.13 scope.
- `Next steps`: either fix action-required rows, choose an explicit child repo, or continue to the user-intent workflow.
- `Next steps`: either fix action-required rows, choose an explicit child repo, continue to the user-intent workflow, or suggest `spec-rule-miner` as a separate follow-up after CodeGraph/Graphify readiness is prepared. This suggestion is advisory; setup must not treat rule-miner output as setup readiness and must not call `spec-rule-miner` automatically.

@@ -191,2 +191,3 @@ `tool-facts.json` records setup-owned tool and helper readiness:

- treat setup facts as semantic code evidence;
- invoke `spec-rule-miner`, synthesize project rules, or write `docs/ai/project-rules.md`;
- treat `.spec-first/config.local.yaml` as team-shared workflow policy;

@@ -193,0 +194,0 @@ - decide issue/PR category, state, scope, accept/reject status, or implementation truth;

@@ -201,4 +201,77 @@ {

"reason_code": "frontmatter-preservation"
},
{
"id": "refresh-noop-idempotence",
"case_id": "SPEC-RULE-MINER-WRITE-004",
"case_type": "expected",
"input": "Refresh the mined project rules. docs/ai/project-rules.md already has a spec-rule-miner marked block, and AGENTS.md / CLAUDE.md already point to it. The newly mined candidate rules are substantively unchanged.",
"prompt": "Refresh the mined project rules. docs/ai/project-rules.md already has a spec-rule-miner marked block, and AGENTS.md / CLAUDE.md already point to it. The newly mined candidate rules are substantively unchanged.",
"coverage_tags": ["write-safety", "refresh", "idempotence"],
"expected_outcome": "refresh-noop",
"expected_result": "refresh-noop",
"expected_mode": "compare-candidate-to-canonical-block-and-write-nothing",
"boundary_note": "Non-first runs still re-mine and compare; unchanged rules and correct pointers should close out as refresh_noop without rewriting files.",
"required_signals": [
"generate candidate rules before deciding",
"compare with canonical marked block",
"record refresh_noop",
"preserve existing pointers"
],
"forbidden_signals": [
"exit only because project-rules.md exists",
"rewrite timestamp-only changes",
"rewrite AGENTS.md when pointer is already correct",
"skip evidence summary"
],
"reason_code": "refresh-noop-idempotence"
},
{
"id": "large-repo-graph-candidate-boundary",
"case_id": "SPEC-RULE-MINER-EXPECTED-002",
"case_type": "expected",
"input": "Mine project rules for a very large monorepo where a code-graph/project-graph provider returns candidate hidden associations between API handlers, validators, and route registries.",
"prompt": "Mine project rules for a very large monorepo where a code-graph/project-graph provider returns candidate hidden associations between API handlers, validators, and route registries.",
"coverage_tags": ["expected", "scope", "large-repo", "capability-class", "provider-boundary"],
"expected_outcome": "use-provider-candidates-as-navigation-only",
"expected_result": "use-provider-candidates-as-navigation-only",
"expected_mode": "provider-untrusted-candidates-then-source-confirmation",
"boundary_note": "code-graph/project-graph candidates can shrink source reads in large repos but cannot prove rules, hidden associations, frequency, or package scope.",
"required_signals": [
"provider_untrusted",
"source confirmation before rule text",
"fallback to bounded source reads",
"record accepted and rejected candidates"
],
"forbidden_signals": [
"cite graph edge as rule evidence",
"treat provider candidate as confirmed frequency",
"refresh or rebuild the graph from this skill",
"cat graph.json"
],
"reason_code": "large-repo-provider-candidate-boundary"
},
{
"id": "no-analyzable-source-failure",
"case_id": "SPEC-RULE-MINER-FAILURE-001",
"case_type": "failure",
"input": "Generate project rules for this repository, but the target contains only lockfiles, vendored generated code, and build artifacts.",
"prompt": "Generate project rules for this repository, but the target contains only lockfiles, vendored generated code, and build artifacts.",
"coverage_tags": ["failure", "source-evidence"],
"expected_outcome": "do-not-generate-empty-rules",
"expected_result": "do-not-generate-empty-rules",
"expected_mode": "stop-with-limitations",
"boundary_note": "No current human-written source means there is no evidence-backed rules_block to write.",
"required_signals": [
"no analyzable source",
"do not generate empty rules",
"limitations"
],
"forbidden_signals": [
"write generic best practices",
"infer rules from generated code",
"create empty project-rules.md"
],
"reason_code": "no-analyzable-source"
}
]
}

@@ -17,2 +17,12 @@ # Pattern Categories

## 大仓候选导航
大仓库、monorepo 或 hidden association 难以直接定位时,可以把 `code-graph` / `project-graph` capability-class 输出作为 `provider_untrusted` 候选导航,用来决定下一批 source refs。遵守当前仓库 `docs/contracts/project-graph-consumption.md` 的 candidate-only 口径;若该合同在运行目标不可见,按本小节最小边界执行。
- 候选只回答“先看哪里”,不能证明规则、频率、80% 一致性、包级适用范围、hidden association 或 anti-pattern。
- 每条进入规则正文的模式仍必须由当前目标仓库源码、测试、配置或已有人写规则文件确认;记录代表性 source refs。
- 候选不可用、stale、unknown、unverified、失败或不安全时,直接回退到 bounded source reads、`rg`、ast-grep 和分层抽样;不要阻塞规则挖掘。
- 不从本 skill 运行图谱刷新、索引生成、repair 或 mutation;不要读取完整 raw graph artifact,例如 `graph.json`。
- 如果候选影响了阅读顺序,在 preview/closeout 的 `limitations` 或 `evidence_summary` 里说明查询摘要、采纳/拒绝的候选和回源确认结果。
## 必查类别

@@ -19,0 +29,0 @@

@@ -42,4 +42,6 @@ # Write Targets

- 文件不存在:创建父目录和文件,只写目标块或 pointer。
- 非首次 refresh:先读取 `docs/ai/project-rules.md` 的当前 marked block、入口文件 pointer 和用户指定目标,再生成 candidate rules block;candidate 与当前 canonical block 无实质变化且 pointer 已正确时,不写任何文件,closeout 记录 `refresh_noop`、采样范围和限制。
- 独立规则文件中 `spec-rule-miner-start` / `spec-rule-miner-end` markers 存在:只替换 markers 中间内容,保留文件其他部分。
- pointer 文件中 `spec-rule-miner-start` / `spec-rule-miner-end` markers 存在:只替换 markers 中间内容为 pointer,不把完整规则写进入口文件。
- candidate rules block 有变化时:preview 展示规则差异和受影响目标;确认后只替换 marker 中间内容,不因排序、更新时间、同义措辞或 pointer 已正确而重写无变化文件。
- legacy `rule-miner-start` / `rule-miner-end` markers 存在:如果在独立规则文件内,替换旧 markers 与其中内容为新的 `spec-rule-miner` marked block;如果在 `AGENTS.md` / `CLAUDE.md` 内,先把完整规则写入独立规则文件,再把入口文件中的旧块替换为 pointer,并在 summary 说明完成 legacy marker migration。

@@ -46,0 +48,0 @@ - markers 不存在且已有内容明显无关:追加 marked block,不删除用户内容。

@@ -34,3 +34,3 @@ ---

- `target_files`:默认独立规则文件、入口 pointer 文件与用户指定输出文件,说明 pointer 还是 inline。
- `limitations`:小样本、大仓库/多包抽样、混合语言、生成代码占比高、历史例外、冲突模式跳过、headless 默认写入等限制。
- `limitations`:小样本、大仓库/多包抽样、混合语言、生成代码占比高、历史例外、冲突模式跳过、图候选未回源、refresh no-op、headless 默认写入等限制。

@@ -43,2 +43,3 @@ ## Hard Boundaries

- 不覆盖用户已有规则。读取目标文件后,只替换 `spec-rule-miner` markers 内的旧块;无 markers 时追加;疑似旧版无 marker 输出时先询问替换还是追加。
- 非首次执行必须先重新取证并生成 candidate rules block,再与现有 canonical marked block / pointer 对比;无实质变化时不重写文件,closeout 记录 `refresh_noop`、采样范围和限制;有变化时 preview diff 后只替换 marker 内内容。
- 每条规则必须有当前仓库证据:默认至少 2 个文件支撑;小仓库样本不足时降级说明 sample-size;不确定或 50/50 分裂的模式不写成规则。

@@ -52,5 +53,5 @@ - 不泄露敏感信息:密钥、内部 URL、私有包名、账号、生产路径、安全实现细节只用于判断,不进入规则正文。

3. 过滤读取范围:跳过依赖、构建产物、锁文件、minified 文件、二进制、vendored/generated 代码;大仓库或多包仓库使用分层抽样并在 preview 和 closeout 中披露样本包、未覆盖子项目和适用范围。
4. 读取并记录证据。抽取前先读 [Pattern Categories](references/pattern-categories.md),用其中类别组织证据;配置已强制的 formatter/linter 规则只记录为“已由工具处理”,不要重复写入 AI 规则。
4. 读取并记录证据。抽取前先读 [Pattern Categories](references/pattern-categories.md),用其中类别组织证据;大仓库可按其中 capability-class 边界使用 `code-graph` / `project-graph` 候选缩小阅读范围,但规则证据必须回到当前源码;配置已强制的 formatter/linter 规则只记录为“已由工具处理”,不要重复写入 AI 规则。
5. 合成规则:按 `frequency x deviation from defaults` 排序,保留高频且偏离默认的做法;多包规则必须区分跨包通用模式、包级专属模式和历史例外,旧项目反例只能写成“新增代码优先”或“不扩大例外”;除非证据在适用范围内压倒性一致,不使用全仓库绝对措辞。必须包含至少一个 hidden association 和至少一个 anti-pattern,除非证据明确不存在,并在 preview 限制中说明。规则正文可以包含适用范围和例外边界,这不算挖掘过程元说明。
6. Preview:展示将写入独立规则文件的规则块、入口引用文件、word count、采样/证据限制、适用包范围、历史例外,以及每个规则组的代表性 source refs。规则正文不要包含挖掘过程元说明。
6. Preview:展示将写入独立规则文件的规则块、入口引用文件、word count、采样/证据限制、适用包范围、历史例外、refresh diff/no-op 判断,以及每个规则组的代表性 source refs。规则正文不要包含挖掘过程元说明。
7. 写入前读 [Write Targets](references/write-targets.md),按目标文件的 marker、frontmatter、pointer/inline 规则执行。默认把完整规则写入 `docs/ai/project-rules.md`,并让 `AGENTS.md` 与 `CLAUDE.md` 指向该文件。

@@ -65,2 +66,3 @@ 8. 收尾输出:列出写入文件、规则字数、是否采样、未写入的近邻工具文件、需要用户手动检查的限制;如果没有写文件,说明 preview-only 状态;如果因 headless 走默认写入,必须说明 `headless_default_write` 的证据来源。

- 证据不足、模式冲突或生成代码占比过高:降级为 limitations,不把不确定模式写成规则。
- 重新挖掘后与现有 marked block 无实质变化:不要为更新时间戳、排序或同义改写而重写文件;输出 `refresh_noop` 和本次验证过的 source refs / limitations。

@@ -67,0 +69,0 @@ ## Quality Checks

@@ -5,2 +5,10 @@ const crypto = require('node:crypto');

// Windows rename-over-existing is not atomic and can transiently fail with EPERM/EACCES/
// EBUSY when the destination is briefly held by an antivirus real-time scan, the search
// indexer, or another open handle. POSIX rename() has no such contention. Retry the rename
// a few times with a short backoff on those Windows-transient codes before giving up.
const RENAME_RETRY_CODES = new Set(['EPERM', 'EACCES', 'EBUSY']);
const RENAME_RETRY_ATTEMPTS = 10;
const RENAME_RETRY_DELAY_MS = 20;
function createAtomicTempPath(filePath) {

@@ -13,2 +21,29 @@ const dir = path.dirname(filePath);

function sleepMsBlocking(ms) {
// Synchronous backoff without adding an async signature to the shared write helper.
// Atomicity of the rename retry does not benefit from yielding the event loop here, and
// the callers (init/state writes) are already synchronous.
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
}
function renameWithWindowsRetry(tmpPath, filePath) {
// Only the destination-contention codes are retried; all other errors (ENOENT, ENOSPC,
// cross-device, etc.) throw immediately so real failures are not masked by the backoff.
if (process.platform !== 'win32') {
fs.renameSync(tmpPath, filePath);
return;
}
for (let attempt = 1; ; attempt += 1) {
try {
fs.renameSync(tmpPath, filePath);
return;
} catch (error) {
if (!RENAME_RETRY_CODES.has(error && error.code) || attempt >= RENAME_RETRY_ATTEMPTS) {
throw error;
}
sleepMsBlocking(RENAME_RETRY_DELAY_MS);
}
}
}
function writeFileAtomic(filePath, contents, encoding = 'utf8') {

@@ -19,3 +54,3 @@ fs.mkdirSync(path.dirname(filePath), { recursive: true });

fs.writeFileSync(tmpPath, contents, encoding);
fs.renameSync(tmpPath, filePath);
renameWithWindowsRetry(tmpPath, filePath);
} catch (error) {

@@ -22,0 +57,0 @@ fs.rmSync(tmpPath, { force: true });

@@ -8,7 +8,64 @@ const fs = require('node:fs');

const SPEC_PLAN_COMMAND_NAME = 'spec-plan';
const SESSION_START_COMMAND = '"$CLAUDE_PROJECT_DIR"/.claude/hooks/session-start';
const SPEC_PLAN_GUARD_COMMAND = '"$CLAUDE_PROJECT_DIR"/.claude/hooks/spec-plan-guard';
const PRD_PREWRITE_GUARD_COMMAND = '"$CLAUDE_PROJECT_DIR"/.claude/hooks/prd-prewrite-guard';
const PRD_READINESS_GUARD_COMMAND = '"$CLAUDE_PROJECT_DIR"/.claude/hooks/prd-readiness-guard';
// Managed Claude hooks use exec form (command + args) instead of a bash shell command
// string. Claude Code runs shell form through Git Bash on Windows and falls back to
// PowerShell when Git Bash is absent — a bash-only string like
// `"$CLAUDE_PROJECT_DIR"/.claude/hooks/session-start` then fails (PowerShell reads
// `$CLAUDE_PROJECT_DIR` as an undefined variable and cannot execute an extensionless
// bash script). Exec form spawns `node` directly with no shell on any platform, so the
// managed hooks work on macOS, Linux, and Windows with or without Git Bash. The hook files
// are Node scripts (see templates/claude/hooks/*).
//
// The hook path in args is PROJECT-ROOT-RELATIVE (`.claude/hooks/xxx`), NOT
// `$CLAUDE_PROJECT_DIR/.claude/hooks/xxx`. In exec form there is no shell, and Claude Code
// does NOT variable-expand `$CLAUDE_PROJECT_DIR` inside args — only dedicated path
// placeholders like `$CLAUDE_PLUGIN_ROOT` are substituted. A literal `$CLAUDE_PROJECT_DIR`
// would be passed to node verbatim and resolved against cwd, producing
// `<cwd>/$CLAUDE_PROJECT_DIR/.claude/hooks/xxx` -> MODULE_NOT_FOUND. Claude Code runs the
// hook with cwd set to the project root, so a relative `.claude/hooks/xxx` resolves
// correctly on every platform, and each hook reads its project dir from
// `CLAUDE_PROJECT_DIR` env / stdin `cwd` / `process.cwd()`.
const HOOK_INTERPRETER = 'node';
const SESSION_START_HOOK_PATH = '.claude/hooks/session-start';
const SPEC_PLAN_GUARD_HOOK_PATH = '.claude/hooks/spec-plan-guard';
const PRD_PREWRITE_GUARD_HOOK_PATH = '.claude/hooks/prd-prewrite-guard';
const PRD_READINESS_GUARD_HOOK_PATH = '.claude/hooks/prd-readiness-guard';
// Legacy bash shell-form commands from before the exec-form migration. Kept only so that
// detection/removal still recognizes and cleans a pre-migration managed hook on refresh.
const LEGACY_SESSION_START_COMMAND = '"$CLAUDE_PROJECT_DIR"/.claude/hooks/session-start';
const LEGACY_SPEC_PLAN_GUARD_COMMAND = '"$CLAUDE_PROJECT_DIR"/.claude/hooks/spec-plan-guard';
const LEGACY_PRD_PREWRITE_GUARD_COMMAND = '"$CLAUDE_PROJECT_DIR"/.claude/hooks/prd-prewrite-guard';
const LEGACY_PRD_READINESS_GUARD_COMMAND = '"$CLAUDE_PROJECT_DIR"/.claude/hooks/prd-readiness-guard';
const LEGACY_MANAGED_COMMANDS = [
LEGACY_SESSION_START_COMMAND,
LEGACY_SPEC_PLAN_GUARD_COMMAND,
LEGACY_PRD_PREWRITE_GUARD_COMMAND,
LEGACY_PRD_READINESS_GUARD_COMMAND,
];
// Preserved export name for backward compatibility with consumers/tests that referenced the
// session-start command constant. Now points at the exec-form hook path token.
const SESSION_START_COMMAND = SESSION_START_HOOK_PATH;
const SPEC_PLAN_GUARD_COMMAND = SPEC_PLAN_GUARD_HOOK_PATH;
const PRD_PREWRITE_GUARD_COMMAND = PRD_PREWRITE_GUARD_HOOK_PATH;
const PRD_READINESS_GUARD_COMMAND = PRD_READINESS_GUARD_HOOK_PATH;
const MANAGED_HOOK_PATH_PATTERN = /(^|[^A-Za-z0-9_])\.claude\/hooks\/(?:session-start|spec-plan-guard|prd-prewrite-guard|prd-readiness-guard)(\s|"|$)/;
// Current (relative) exec-form arg paths.
const MANAGED_HOOK_ARG_PATHS = [
SESSION_START_HOOK_PATH,
SPEC_PLAN_GUARD_HOOK_PATH,
PRD_PREWRITE_GUARD_HOOK_PATH,
PRD_READINESS_GUARD_HOOK_PATH,
];
// Legacy exec-form arg paths from the first exec-form migration, which incorrectly prefixed
// a literal `$CLAUDE_PROJECT_DIR/` (that Claude never expands). Removal must still recognize
// these so a refresh replaces the broken hook instead of leaving a duplicate alongside the
// corrected one.
const LEGACY_EXEC_FORM_ARG_PATHS = MANAGED_HOOK_ARG_PATHS.map((p) => `$CLAUDE_PROJECT_DIR/${p}`);
const REMOVABLE_EXEC_FORM_ARG_PATHS = new Set([
...MANAGED_HOOK_ARG_PATHS,
...LEGACY_EXEC_FORM_ARG_PATHS,
]);

@@ -38,11 +95,19 @@ const MANAGED_HOOK_DEFINITIONS = [

// Exec-form managed hook: `node .claude/hooks/<name>`. No shell runs, so this is Windows-
// safe with or without Git Bash. The single args element is the project-root-relative hook
// path, which resolves against Claude Code's hook cwd (the project root). The hook derives
// its own project dir from CLAUDE_PROJECT_DIR env / stdin `cwd` / process.cwd() rather than
// a literal $CLAUDE_PROJECT_DIR arg (exec form does not expand it).
function buildExecFormHook(hookPath) {
return {
type: 'command',
command: HOOK_INTERPRETER,
args: [hookPath],
};
}
function buildManagedSessionStartMatcher() {
return {
matcher: SESSION_START_MATCHER,
hooks: [
{
type: 'command',
command: SESSION_START_COMMAND,
},
],
hooks: [buildExecFormHook(SESSION_START_HOOK_PATH)],
};

@@ -54,8 +119,3 @@ }

matcher: SPEC_PLAN_COMMAND_NAME,
hooks: [
{
type: 'command',
command: SPEC_PLAN_GUARD_COMMAND,
},
],
hooks: [buildExecFormHook(SPEC_PLAN_GUARD_HOOK_PATH)],
};

@@ -67,8 +127,3 @@ }

matcher: 'Write|Edit|MultiEdit',
hooks: [
{
type: 'command',
command: PRD_PREWRITE_GUARD_COMMAND,
},
],
hooks: [buildExecFormHook(PRD_PREWRITE_GUARD_HOOK_PATH)],
};

@@ -80,35 +135,55 @@ }

matcher: '.*',
hooks: [
{
type: 'command',
command: PRD_READINESS_GUARD_COMMAND,
},
],
hooks: [buildExecFormHook(PRD_READINESS_GUARD_HOOK_PATH)],
};
}
// True when any exec-form args element references a managed hook path. Exec form stores the
// hook path as a plain args string (not the command, which is the `node` interpreter), so
// detection/removal must scan args in addition to the legacy command string.
function execFormArgsReferenceManagedHook(hook) {
return !!hook
&& Array.isArray(hook.args)
&& hook.args.some((arg) => typeof arg === 'string' && MANAGED_HOOK_PATH_PATTERN.test(arg));
}
// Loose substring match: used for drift DETECTION/inspection so a lightly-edited managed
// command is still recognized as ours and reported as drifted.
// command is still recognized as ours and reported as drifted. Recognizes both the current
// exec form (node + args hook path) and the legacy bash shell-form command string.
function isSpecFirstManagedHook(hook) {
return !!hook &&
typeof hook === 'object' &&
hook.type === 'command' &&
typeof hook.command === 'string' &&
MANAGED_HOOK_PATH_PATTERN.test(hook.command);
if (!hook || typeof hook !== 'object' || hook.type !== 'command') {
return false;
}
if (typeof hook.command === 'string' && MANAGED_HOOK_PATH_PATTERN.test(hook.command)) {
return true;
}
return execFormArgsReferenceManagedHook(hook);
}
// Tight match: used for REMOVAL only. The Claude managed commands are project-relative
// and stable, so exact-equality (or the command followed by extra args, e.g. spec-first's
// own "...session-start --debug" drift) is safe to remove and re-add. A user wrapper that
// merely references the managed path mid-command (e.g. "my-wrapper ...session-start && x")
// does not start with the managed command, so it is preserved instead of silently deleted.
// Tight match: used for REMOVAL only. Removes both the current exec-form managed hooks
// (command === 'node' and an args element is exactly a managed hook path) and legacy
// shell-form managed commands (exact-equality or the command followed by extra args). A
// user wrapper that merely references the managed path mid-command does not match, so it is
// preserved instead of silently deleted.
function isManagedHookForRemoval(hook) {
if (!hook || typeof hook !== 'object' || hook.type !== 'command' || typeof hook.command !== 'string') {
if (!hook || typeof hook !== 'object' || hook.type !== 'command') {
return false;
}
return [SESSION_START_COMMAND, SPEC_PLAN_GUARD_COMMAND, PRD_PREWRITE_GUARD_COMMAND].some((command) => (
hook.command === command || hook.command.startsWith(`${command} `)
)) || [PRD_READINESS_GUARD_COMMAND].some((command) => (
hook.command === command || hook.command.startsWith(`${command} `)
));
// Exec form: node + args containing a managed hook path. Matches both the current
// relative paths and the legacy `$CLAUDE_PROJECT_DIR/`-prefixed paths so a refresh
// replaces a broken legacy exec-form hook instead of leaving a duplicate.
if (hook.command === HOOK_INTERPRETER && Array.isArray(hook.args)) {
if (hook.args.some((arg) => REMOVABLE_EXEC_FORM_ARG_PATHS.has(arg))) {
return true;
}
}
// Legacy shell form: the managed command string, optionally followed by extra args.
if (typeof hook.command === 'string') {
return LEGACY_MANAGED_COMMANDS.some((command) => (
hook.command === command || hook.command.startsWith(`${command} `)
));
}
return false;
}

@@ -391,2 +466,3 @@

actual.hooks[0].command === expected.hooks[0].command &&
stringArraysEqual(actual.hooks[0].args, expected.hooks[0].args) &&
Object.keys(actual).length === Object.keys(expected).length &&

@@ -396,2 +472,9 @@ Object.keys(actual.hooks[0]).length === Object.keys(expected.hooks[0]).length;

function stringArraysEqual(actual, expected) {
const actualArr = Array.isArray(actual) ? actual : [];
const expectedArr = Array.isArray(expected) ? expected : [];
return actualArr.length === expectedArr.length
&& actualArr.every((value, index) => value === expectedArr[index]);
}
function cloneJson(value) {

@@ -398,0 +481,0 @@ return JSON.parse(JSON.stringify(value || {}));

@@ -143,3 +143,3 @@ const path = require('node:path');

return {
args: ['init', '--all-repos', '-y'],
args: ['init', '-y'],
cwd: root,

@@ -160,4 +160,5 @@ reason_code: 'parent-workspace',

console.error('Fallback commands:');
console.error(' Single repo: spec-first init -y');
console.error(' Parent workspace: spec-first init --all-repos -y');
console.error(' Single repo: spec-first init -y -u <name>');
console.error(' Parent workspace: spec-first init -y -u <name>');
console.error(' Child repo: spec-first init --repo <path> -y -u <name>');
}

@@ -164,0 +165,0 @@

@@ -68,3 +68,3 @@ const fs = require('node:fs');

throw new Error(
'Unable to determine developer name. Run `spec-first init` in an interactive terminal and choose a developer name when prompted.',
'Unable to determine developer name. Run `spec-first init` in an interactive terminal and choose a developer name, or pass `-u <name>` for non-interactive setup.',
);

@@ -71,0 +71,0 @@ }

@@ -108,2 +108,9 @@ 'use strict';

{
prefix: '.qoder/commands/spec-',
match: 'starts_with',
kind: 'generated_runtime_mirror',
reason_code: 'generated_runtime_mirror_excluded',
reason: 'generated runtime mirrors are excluded from ordinary context',
},
{
prefix: '.qoder/commands/spec',

@@ -571,5 +578,8 @@ kind: 'generated_runtime_mirror',

function findExclusion(displayPath) {
return EXCLUDED_PREFIXES.find((entry) => (
displayPath === entry.prefix || displayPath.startsWith(`${entry.prefix}/`)
));
return EXCLUDED_PREFIXES.find((entry) => {
if (entry.match === 'starts_with') {
return displayPath.startsWith(entry.prefix);
}
return displayPath === entry.prefix || displayPath.startsWith(`${entry.prefix}/`);
});
}

@@ -576,0 +586,0 @@

@@ -14,2 +14,4 @@ 'use strict';

'.agents/skills/',
'.cursor/skills/',
'.cursor/spec-first/',
'.kiro/skills/',

@@ -29,2 +31,5 @@ '.kiro/agents/',

'.agents/skills',
'.cursor/skills',
'.cursor/spec-first',
'.cursor/mcp.json',
'.kiro/skills',

@@ -31,0 +36,0 @@ '.kiro/agents',

@@ -9,3 +9,4 @@ const MESSAGES = {

workspaceTarget: '选择 workspace 初始化目标:',
workspaceAllRepos: (count) => `所有子仓库 (${count})`,
workspaceRootOnly: (count) => `仅父级 workspace(推荐,检测到 ${count} 个子仓库)`,
workspaceAllRepos: (count) => `所有子仓库(高级批量维护,${count} 个)`,
workspaceCancel: '取消',

@@ -67,3 +68,4 @@ reuseGlobalProfile: (name, lang) => (

workspaceTarget: 'Select workspace target:',
workspaceAllRepos: (count) => `All child repos (${count})`,
workspaceRootOnly: (count) => `Parent workspace only (recommended, ${count} child repos detected)`,
workspaceAllRepos: (count) => `All child repos (advanced batch maintenance, ${count})`,
workspaceCancel: 'Cancel',

@@ -70,0 +72,0 @@ reuseGlobalProfile: (name, lang) => (

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet