@erclx/aitk 0.111.1 → 1.0.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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "aitk",
3
3
  "description": "Automated governance, versioning, and discovery tools for Claude Code.",
4
- "version": "0.111.1",
4
+ "version": "1.0.0",
5
5
  "author": {
6
6
  "name": "Eric Le",
7
7
  "url": "https://github.com/erclx"
@@ -92,17 +92,19 @@ AITK_NON_INTERACTIVE=1 aitk init \
92
92
 
93
93
  Omit any flag whose resolved value is empty.
94
94
 
95
- Step 2: `aitk tooling sync <tooling-stack>` installs stack deps, scripts, gitignore entries, seeds, golden configs, and drops the reference doc. The extends chain is walked, so syncing `vite-react` also pulls `web` and `base` configs. Skip if the tooling stack is `base` (already synced by `aitk init`).
95
+ Step 2: `aitk tooling sync <tooling-stack> --write` installs stack deps, scripts, gitignore entries, seeds, golden configs, and drops the reference doc. The extends chain is walked, so syncing `vite-react` also pulls `web` and `base` configs. Skip if the tooling stack is `base` (already synced by `aitk init`).
96
+
97
+ `--write` is required. A headless run without it reports what it would replace and exits 1, since golden configs overwrite whatever the target holds at those paths. Scaffolding into a fresh target has nothing to lose, so pass it directly rather than reading a report first.
96
98
 
97
99
  ```bash
98
- AITK_NON_INTERACTIVE=1 aitk tooling sync <tooling-stack> <target>
100
+ AITK_NON_INTERACTIVE=1 aitk tooling sync <tooling-stack> <target> --write
99
101
  ```
100
102
 
101
103
  Monorepo with multiple language roots: run `aitk init` once at the repo root so `base` (husky, prettier, cspell, commitlint, CI) lands single, then sync each subtree with `--skip base` so the shared layer is not re-dropped.
102
104
 
103
105
  ```bash
104
- AITK_NON_INTERACTIVE=1 aitk tooling sync vite-react ./frontend --skip base
105
- AITK_NON_INTERACTIVE=1 aitk tooling sync python ./backend --skip base
106
+ AITK_NON_INTERACTIVE=1 aitk tooling sync vite-react ./frontend --skip base --write
107
+ AITK_NON_INTERACTIVE=1 aitk tooling sync python ./backend --skip base --write
106
108
  ```
107
109
 
108
110
  Without `--skip base`, each subtree re-drops husky, and git honors only one `core.hooksPath`, so the extra hook dirs silently break. Each subtree keeps its own framework configs and its own `.claude/tooling/<stack>.md` audit docs.
@@ -11,7 +11,7 @@ What each `aitk` sync or install command does to existing files in a target proj
11
11
 
12
12
  | Surface | Command | Effect on existing files |
13
13
  | -------------------------------------------------------- | ------------------------ | ---------------------------------------------------------------- |
14
- | Golden configs (eslint, prettier, vite, tsconfig, ruff) | `aitk tooling sync` | Always overwritten. Local edits are lost. Drift is intentional. |
14
+ | Golden configs, listed in full below | `aitk tooling sync` | Overwritten once `--write` is passed. Local edits are lost. |
15
15
  | Dictionary seeds (`.cspell/*.txt`) | `aitk tooling sync` | Merged and sorted. Existing terms preserved. |
16
16
  | Other seeds (`cspell.json`, `.lintstagedrc`, state docs) | `aitk tooling sync` | Copy-once. Dropped on first install, untouched after. |
17
17
  | Standards | `aitk standards install` | All overwritten. |
@@ -22,9 +22,77 @@ What each `aitk` sync or install command does to existing files in a target proj
22
22
  | `.gitignore`, deps, scripts | any sync | Additive. Existing entries preserved. Deps re-pin on major skew. |
23
23
  | Generated `index.md` | any sync or regen | Rewritten from target state. Hand edits are lost. |
24
24
 
25
+ ## What a tooling sync can overwrite
26
+
27
+ A golden config is any file a stack ships under `configs/`, and the category is wider than its name suggests. It carries the CI workflow, the git hooks, the end-to-end harness, the shell scripts under `scripts/`, and the editor settings, alongside the linters and compilers a reader expects. A stack inherits its parent's configs, so syncing `astro` also writes everything `web` and `base` hold.
28
+
29
+ Run `aitk tooling sync <stack> <target> --check` for the list resolved against a real target. It reports every path and writes nothing. The list below is what the stacks hold as shipped, before any chain resolution.
30
+
31
+ <!-- generated:tooling-paths -->
32
+
33
+ ### astro
34
+
35
+ - `astro.config.mjs`
36
+ - `eslint.config.js`
37
+ - `playwright.config.ts`
38
+ - `tsconfig.json`
39
+ - `vitest.config.ts`
40
+
41
+ ### base
42
+
43
+ - `.editorconfig`
44
+ - `.github/pull_request_template.md`
45
+ - `.github/workflows/verify.yml`
46
+ - `.husky/commit-msg`
47
+ - `.husky/post-merge`
48
+ - `.husky/post-rewrite`
49
+ - `.husky/pre-commit`
50
+ - `.husky/pre-push`
51
+ - `.prettierrc`
52
+ - `.shellcheckrc`
53
+ - `.vscode/extensions.json`
54
+ - `.vscode/settings.json`
55
+ - `commitlint.config.js`
56
+ - `scripts/clean.sh`
57
+ - `scripts/update.sh`
58
+ - `scripts/verify.sh`
59
+
60
+ ### python
61
+
62
+ - `.coveragerc`
63
+ - `.python-version`
64
+ - `mypy.ini`
65
+ - `pytest.ini`
66
+ - `ruff.toml`
67
+ - `scripts/verify.sh`
68
+
69
+ ### vite-react
70
+
71
+ - `playwright.config.ts`
72
+ - `tsconfig.json`
73
+ - `vite.config.ts`
74
+ - `vitest.config.ts`
75
+
76
+ ### web
77
+
78
+ - `.github/workflows/verify.yml`
79
+ - `.vscode/extensions.json`
80
+ - `.vscode/settings.json`
81
+ - `e2e/home.spec.ts`
82
+ - `e2e/screenshot.ts`
83
+ - `eslint.config.js`
84
+ - `scripts/screenshot.sh`
85
+ - `scripts/verify.sh`
86
+ - `scripts/worktree-port.sh`
87
+ - `src/test/setup.ts`
88
+
89
+ <!-- /generated:tooling-paths -->
90
+
25
91
  ## Rules
26
92
 
27
- - Before `aitk tooling sync`, know golden configs always overwrite. When the project carries local edits to a golden config, warn the user before running it.
93
+ - `aitk tooling sync` writes nothing until `--write` is passed. A headless run without it reports and exits 1, so a script that forgets the flag fails rather than silently skipping the sync.
94
+ - Run `--check` first when the project carries local edits to any path above. The report names each file it would replace, which is the warning the user needs before the write.
95
+ - An interactive run still prompts. `--write` skips the prompt, and `--check` refuses to write even with a TTY.
28
96
  - Seeds are user-owned. Dictionary `.txt` files merge and sort. Other seeds are copy-once, so re-seeding a structured file means deleting it and syncing again.
29
97
  - Prefer `aitk standards sync` over `install` on an existing project. `install` overwrites every standard.
30
98
  - For section-level customizations of a standard or seed doc, use the `claude-seed-sync` skill, not `aitk ... sync`. It diffs per section and preserves edits.
@@ -77,8 +77,11 @@ surface is in `skills-audit.md`.
77
77
  # Create a new tooling stack
78
78
  AITK_NON_INTERACTIVE=1 aitk tooling create astro
79
79
 
80
- # Sync a stack into a target project
81
- AITK_NON_INTERACTIVE=1 aitk tooling sync astro /path/to/project
80
+ # Report what a stack would change, writing nothing
81
+ AITK_NON_INTERACTIVE=1 aitk tooling sync astro /path/to/project --check
82
+
83
+ # Sync a stack into a target project, overwriting its golden configs
84
+ AITK_NON_INTERACTIVE=1 aitk tooling sync astro /path/to/project --write
82
85
 
83
86
  # Install a governance stack (the stack argument is required headlessly)
84
87
  AITK_NON_INTERACTIVE=1 aitk gov install astro --add 260-shadcn /path/to/project
@@ -90,7 +93,7 @@ AITK_NON_INTERACTIVE=1 aitk gov sync /path/to/project
90
93
  AITK_NON_INTERACTIVE=1 aitk gov build /path/to/project
91
94
 
92
95
  # Sync a monorepo subtree, skipping the base layer the repo root already owns
93
- AITK_NON_INTERACTIVE=1 aitk tooling sync vite-react /path/to/repo/frontend --skip base
96
+ AITK_NON_INTERACTIVE=1 aitk tooling sync vite-react /path/to/repo/frontend --skip base --write
94
97
 
95
98
  # Verify a stack end-to-end in a throwaway scaffold
96
99
  aitk tooling verify vite-react
@@ -134,3 +137,11 @@ SANDBOX_SCENARIO=sync aitk sandbox infra:tooling
134
137
  # Read every audit as one record. Exit 2 is a fact, 3 an audit that did not report
135
138
  aitk audits run --json
136
139
  ```
140
+
141
+ `aitk tooling sync` is the one verb above whose flag is mandatory headlessly. It
142
+ overwrites every golden config a stack ships, which reaches the CI workflow, the
143
+ git hooks, the end-to-end harness, and the shell scripts under `scripts/`, so a
144
+ headless run carrying neither `--check` nor `--write` reports what it would
145
+ replace and exits 1 rather than applying it. Run `--check` first to read the
146
+ list, then `--write` to apply it. `aitk tooling sync --help` names both, and the
147
+ full per-stack path list sits in the `toolkit-cli` skill.
@@ -45,7 +45,7 @@ Two steps, in order:
45
45
  The chain is:
46
46
 
47
47
  - `aitk init` installs base tooling, Claude seeds, and governance rules into `.claude/rules/` in the same pass
48
- - `aitk tooling sync <stack>` adds stack-specific deps, scripts, gitignore entries, and drops `.claude/tooling/<stack>.md` (plus parents) as the agent's audit context
48
+ - `aitk tooling sync <stack> --write` adds stack-specific deps, scripts, gitignore entries, and drops `.claude/tooling/<stack>.md` (plus parents) as the agent's audit context
49
49
  - The agent follows the reference to generate eslint, vitest, playwright configs and the stack's setup script, and extends `.claude/context/ci.md` and `.claude/context/development.md` per the reference's extend sections <!-- audit-ignore-citations -->
50
50
  - `setup-verify` runs the installed `package.json` scripts (lint, typecheck, check, test, build) and reports pass or fail
51
51
 
@@ -148,7 +148,9 @@ The same field names a proposal-only skill with a live case here under `migratio
148
148
 
149
149
  Add `--json` for the machine-readable report, and `--exit-code` to fail a CI job when a target falls behind. Files the project authored itself never count toward that exit code, and neither do superseded artifacts, seed drift, tooling, or anything the reverse walk reports, since each names content the project is expected to edit or place itself. An unmigrated domain does count, because running the relocation closes it.
150
150
 
151
- Tooling reports under a section of its own, and `measured` there says whether the target ever recorded a chain. One that never ran a tooling sync reports unmeasured rather than clean, which is what separates tooling nobody has looked at from tooling that is current. A workspace root records nothing either way, since each package resolves its own chain. Reconcile the configs with `aitk tooling sync <stack> <path>`.
151
+ Tooling reports under a section of its own, and `measured` there says whether the target ever recorded a chain. One that never ran a tooling sync reports unmeasured rather than clean, which is what separates tooling nobody has looked at from tooling that is current. A workspace root records nothing either way, since each package resolves its own chain.
152
+
153
+ Reconcile the configs with `aitk tooling sync <stack> <path> --check` to read which files differ, then re-run it with `--write` to apply them. The drift report counts categories and the sync names paths, which is the difference worth knowing before a golden config the project edited is replaced.
152
154
 
153
155
  ### Catch-all
154
156
 
@@ -211,8 +213,8 @@ The repo root owns the shared `base` layer, and each language lives in its own s
211
213
 
212
214
  ```bash
213
215
  aitk init --stack react .
214
- aitk tooling sync vite-react ./frontend --skip base
215
- aitk tooling sync python ./backend --skip base
216
+ aitk tooling sync vite-react ./frontend --skip base --write
217
+ aitk tooling sync python ./backend --skip base --write
216
218
  ```
217
219
 
218
220
  `--skip base` drops the `base` layer from each subtree sync, so husky, prettier, cspell, commitlint, and CI stay single at the repo root. Without it, every subtree re-drops husky, and since git honors only one `core.hooksPath` the extra hook dirs silently break. Each subtree still gets its own framework configs (eslint, vitest, tsconfig, vite) and its own `.claude/tooling/<stack>.md` audit docs.
@@ -0,0 +1,20 @@
1
+ ---
2
+ description: Route .claude/teach/ edits to the teach standard for layout, ordinal naming, and the mission and record formats, in the projects that open a learning workspace
3
+ paths:
4
+ - '.claude/teach/**'
5
+ ---
6
+
7
+ # Teach standards
8
+
9
+ ## What a workspace keeps
10
+
11
+ - Never renumber an existing folder, lesson, or record. A later reader cites it by the name it opened under.
12
+ - Write every success line as a task the learner can be asked to perform, never as something they will understand.
13
+ - Keep a reference page free of the learner. A page carrying second person or a quiz belongs under `lessons/`.
14
+ - Record the wrong answer a learner gave, never a count of what they missed.
15
+ - Date the workspace once, in the `MISSION.md` frontmatter. Never repeat the date in the body of any file.
16
+
17
+ ## Authority
18
+
19
+ - Follow `.claude/standards/teach.md` for the folder layout, ordinal naming, frontmatter, and the mission and learning-record formats. It is the single source.
20
+ - Follow `.claude/standards/glossary.md` for the glossary every workspace carries at its root.
@@ -0,0 +1,19 @@
1
+ ---
2
+ description: Route .claude/tasks/ session map edits to the session standard for the filename, the core sections, and what a handoff carries
3
+ paths:
4
+ - '.claude/tasks/session-*.md'
5
+ ---
6
+
7
+ # Session map standards
8
+
9
+ ## What a handoff carries
10
+
11
+ - Follow this rule rather than `555-tasks.md` for a `session-` file. A handoff is not a task and carries neither `## Outcomes` nor `## Findings`.
12
+ - Never write into a file another session owns. Overwrite only the file the writing session already wrote.
13
+ - Resolve the containing folder at the main worktree root, never inside a linked worktree.
14
+ - Write only what a compaction destroys. Do not restate the board and do not summarize what shipped.
15
+ - Cite a commit, a task, or a file and line for every claim, so a reader can tell a read from a recall.
16
+
17
+ ## Authority
18
+
19
+ - Follow `.claude/standards/session.md` for the filename and location, frontmatter, the core sections, and the write and read procedures. It is the single source.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/aitk",
3
3
  "type": "module",
4
- "version": "0.111.1",
4
+ "version": "1.0.0",
5
5
  "description": "Infrastructure and quality tooling for developer workflows",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env bash
2
+ set -e
3
+ set -o pipefail
4
+
5
+ # Rewrites the generated path block in the shipped overwrite contract, which
6
+ # names every file a tooling sync can replace. The category reaches past the
7
+ # linters its name suggests, into the CI workflow, the end-to-end harness, and
8
+ # the shell scripts under scripts/, so a reader cannot infer the list. Generating
9
+ # it is what keeps a stack gaining a file from leaving the contract wrong.
10
+ #
11
+ # The stack names come from `tooling list` rather than from a directory walk,
12
+ # so a stack the verb excludes never reaches a contract about what the verb does.
13
+
14
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
15
+ PROJECT_ROOT="${PROJECT_ROOT:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
16
+
17
+ TOOLING_DIR="$PROJECT_ROOT/tooling"
18
+ CONTRACT="$PROJECT_ROOT/claude/skills/toolkit-cli/SKILL.md"
19
+
20
+ BEGIN="<!-- generated:tooling-paths -->"
21
+ END="<!-- /generated:tooling-paths -->"
22
+
23
+ [ -d "$TOOLING_DIR" ] || exit 0
24
+ [ -f "$CONTRACT" ] || exit 0
25
+
26
+ grep -qF "$BEGIN" "$CONTRACT" || {
27
+ echo "Missing $BEGIN marker in $CONTRACT" >&2
28
+ exit 1
29
+ }
30
+
31
+ block="$(mktemp)"
32
+ trap 'rm -f "$block" "$CONTRACT.tmp"' EXIT
33
+
34
+ {
35
+ echo "$BEGIN"
36
+ echo
37
+ while IFS= read -r stack; do
38
+ [ -d "$TOOLING_DIR/$stack/configs" ] || continue
39
+ echo "### $stack"
40
+ echo
41
+ while IFS= read -r rel; do
42
+ echo "- \`$rel\`"
43
+ done < <(cd "$TOOLING_DIR/$stack/configs" && find . -type f | sed 's|^\./||' | sort)
44
+ echo
45
+ done < <(bun "$PROJECT_ROOT/src/cli.ts" tooling list --json | jq -r '.stacks[].name' | sort)
46
+ echo "$END"
47
+ } >"$block"
48
+
49
+ # awk streams the file rather than editing in place, so a pattern that matches
50
+ # nothing produces a file missing the block instead of an unchanged one that
51
+ # exits zero.
52
+ awk -v block_file="$block" -v begin="$BEGIN" -v end="$END" '
53
+ $0 == begin { while ((getline line < block_file) > 0) print line; skipping = 1; next }
54
+ $0 == end { skipping = 0; next }
55
+ !skipping { print }
56
+ ' "$CONTRACT" >"$CONTRACT.tmp"
57
+
58
+ # The destination is a tracked file with a mode the copy must not inherit from
59
+ # mktemp, so the content moves onto the original rather than replacing it.
60
+ cat "$CONTRACT.tmp" >"$CONTRACT"
61
+ rm -f "$CONTRACT.tmp"
@@ -272,6 +272,11 @@ main() {
272
272
  assert_no_drift "claude/skills/*/references" "Skill references drifted. Run bun run check and commit the updated reference files."
273
273
  log_info "Skill references clean"
274
274
 
275
+ log_step "Tooling paths"
276
+ run_check "bash $PROJECT_ROOT/scripts/core/regen-tooling-paths.sh" "Tooling-path regen failed"
277
+ assert_no_drift "claude/skills/toolkit-cli/SKILL.md" "The overwrite contract drifted from what the stacks hold. Run bun run check and commit claude/skills/toolkit-cli/SKILL.md."
278
+ log_info "Tooling paths clean"
279
+
275
280
  log_step "Skill paths"
276
281
  run_check "bash $PROJECT_ROOT/scripts/core/check-skill-paths.sh" "Shipped skills reference a repo-local path."
277
282
  log_info "Skill paths clean"
@@ -129,7 +129,7 @@ main() {
129
129
  run_phase "Prepare" bash -c "cd '$tmp_dir' && $prepare"
130
130
  fi
131
131
 
132
- run_phase "Sync" bash -c "cd '$tmp_dir' && AITK_NON_INTERACTIVE=1 aitk tooling sync $stack ."
132
+ run_phase "Sync" bash -c "cd '$tmp_dir' && AITK_NON_INTERACTIVE=1 aitk tooling sync $stack . --write"
133
133
 
134
134
  if [ -f "$tmp_dir/package.json" ]; then
135
135
  run_phase "lint:fix" bash -c "cd '$tmp_dir' && bun run lint:fix"
@@ -218,7 +218,9 @@ function renderTooling(report: CheckReport): void {
218
218
  ? 'Not stamped. No chain recorded, so tooling drift is unmeasured.'
219
219
  : `Recorded chain names no stack this toolkit ships: ${tooling.chain.join(' < ')}.`,
220
220
  )
221
- logInfo('Run `aitk tooling sync <stack>` to record what this target holds.')
221
+ logInfo(
222
+ 'Run `aitk tooling sync <stack> --write` to record what this target holds.',
223
+ )
222
224
  return
223
225
  }
224
226
 
@@ -235,7 +237,9 @@ function renderTooling(report: CheckReport): void {
235
237
  for (const [category, count] of Object.entries(tooling.counts)) {
236
238
  if (count > 0) logWarn(`${count} ${category}`)
237
239
  }
238
- logInfo('Run `aitk tooling sync` to reconcile these.')
240
+ logInfo(
241
+ 'Run `aitk tooling sync --check` to see which files, `--write` to apply.',
242
+ )
239
243
  }
240
244
 
241
245
  /**
@@ -20,7 +20,16 @@ import {
20
20
  } from '@/tooling/manifest'
21
21
  import { scan, type ScanResult } from '@/tooling/scan'
22
22
  import { recordToolingChain } from '@/tooling/stamp'
23
- import { intro, logAdd, logInfo, logStep, logWarn, outro, select } from '@/ui'
23
+ import {
24
+ intro,
25
+ isNonInteractive,
26
+ logAdd,
27
+ logInfo,
28
+ logStep,
29
+ logWarn,
30
+ outro,
31
+ select,
32
+ } from '@/ui'
24
33
 
25
34
  const GREEN = '\x1b[0;32m'
26
35
  const NC = '\x1b[0m'
@@ -30,6 +39,8 @@ const PASS_THROUGH_VERBS = ['ref', 'create', 'verify'] as const
30
39
  interface SyncOptions {
31
40
  readonly ref?: boolean
32
41
  readonly skip?: string
42
+ readonly check?: boolean
43
+ readonly write?: boolean
33
44
  }
34
45
 
35
46
  interface InjectOptions {
@@ -66,6 +77,24 @@ export function register(program: Command): void {
66
77
  .helpOption('-h, --help', 'Show this help message')
67
78
  .option('--no-ref', 'Skip dropping reference docs')
68
79
  .option('--skip <stack>', 'Drop a layer from the extends chain')
80
+ .option('--check', 'Report what would change and write nothing')
81
+ .option('--write', 'Apply every change without prompting')
82
+ .addHelpText(
83
+ 'after',
84
+ [
85
+ '',
86
+ 'Examples:',
87
+ ' aitk tooling sync base',
88
+ ' aitk tooling sync base --check',
89
+ ' AITK_NON_INTERACTIVE=1 aitk tooling sync base --write',
90
+ '',
91
+ 'To gate CI on tooling drift, run headlessly with neither flag. That',
92
+ 'exits 1 when a file would be replaced and 0 when none would, which is',
93
+ 'what `aitk sync --check --exit-code` spells with a flag. Pass --check',
94
+ 'to report the same list and always exit 0.',
95
+ '',
96
+ ].join('\n'),
97
+ )
69
98
  .action(
70
99
  async (stack: string | undefined, target: string, opts: SyncOptions) => {
71
100
  process.exitCode = await runSync(stack, target, opts)
@@ -193,6 +222,12 @@ async function runSync(
193
222
  ): Promise<number> {
194
223
  intro('aitk tooling sync')
195
224
 
225
+ if (opts.check === true && opts.write === true) {
226
+ logWarn('Pass --check or --write, not both.')
227
+ outro()
228
+ return 1
229
+ }
230
+
196
231
  const selected = stack ?? (await promptForStack())
197
232
  if (selected === undefined) {
198
233
  logWarn('No tooling stacks found')
@@ -220,26 +255,33 @@ async function runSync(
220
255
 
221
256
  report(result, includeReferences)
222
257
 
258
+ const mode = resolveWriteMode(opts)
259
+
223
260
  if (result.totalChanges === 0) {
224
- await stampChain(prepared.chain, prepared.target)
261
+ // The stamp is a write like any other, so a run with no authority to write
262
+ // leaves the target's record alone rather than claiming a sync it never
263
+ // performed.
264
+ if (mode === 'apply' || mode === 'prompt') {
265
+ await stampChain(prepared.chain, prepared.target)
266
+ }
225
267
  outro()
226
268
  process.stderr.write(`${GREEN}✓ Everything up to date${NC}\n`)
227
269
  return 0
228
270
  }
229
271
 
230
- const shouldApply = await select({
231
- message: `Apply ${result.totalChanges} changes (${summarize(result)})?`,
232
- options: [
233
- { value: true, label: 'Apply all' },
234
- { value: false, label: 'Cancel' },
235
- ],
236
- nonInteractiveDefault: true,
237
- })
272
+ const decision = await decideApply(result, mode)
238
273
 
239
- if (!shouldApply) {
240
- logWarn('Sync cancelled')
274
+ if (decision !== 'apply') {
275
+ logWarn(
276
+ decision === 'cancelled'
277
+ ? 'Sync cancelled'
278
+ : `Reported ${result.totalChanges} changes (${summarize(result)}). Nothing written.`,
279
+ )
280
+ if (decision === 'unauthorized') {
281
+ logInfo('Re-run with --write to apply them, or --check to silence this.')
282
+ }
241
283
  outro()
242
- return 0
284
+ return decision === 'unauthorized' ? 1 : 0
243
285
  }
244
286
 
245
287
  if (result.configs.some((entry) => entry.state !== 'matching')) {
@@ -265,6 +307,46 @@ async function runSync(
265
307
  return 0
266
308
  }
267
309
 
310
+ type WriteMode = 'apply' | 'prompt' | 'report' | 'unauthorized'
311
+
312
+ type ApplyDecision = 'apply' | 'cancelled' | 'reported' | 'unauthorized'
313
+
314
+ /**
315
+ * Golden configs carry the CI workflow, the end-to-end harness, and the shell
316
+ * scripts under `scripts/`, so an overwrite reaches work no one would consent
317
+ * to losing. A headless caller therefore has no authority to write without
318
+ * `--write`, since a confirm prompt carrying `nonInteractiveDefault` resolves
319
+ * to its first option and would read silence as consent.
320
+ */
321
+ function resolveWriteMode(opts: SyncOptions): WriteMode {
322
+ if (opts.check === true) return 'report'
323
+ if (opts.write === true) return 'apply'
324
+ return isNonInteractive() ? 'unauthorized' : 'prompt'
325
+ }
326
+
327
+ /**
328
+ * Exit 1 follows `unauthorized`, so a caller that forgets `--write` fails
329
+ * rather than reporting a sync it never performed.
330
+ */
331
+ async function decideApply(
332
+ result: ScanResult,
333
+ mode: WriteMode,
334
+ ): Promise<ApplyDecision> {
335
+ if (mode === 'report') return 'reported'
336
+ if (mode === 'apply') return 'apply'
337
+ if (mode === 'unauthorized') return 'unauthorized'
338
+
339
+ const shouldApply = await select({
340
+ message: `Apply ${result.totalChanges} changes (${summarize(result)})?`,
341
+ options: [
342
+ { value: true, label: 'Apply all' },
343
+ { value: false, label: 'Cancel' },
344
+ ],
345
+ })
346
+
347
+ return shouldApply ? 'apply' : 'cancelled'
348
+ }
349
+
268
350
  /**
269
351
  * Writes the chain after the copies land, so a partial apply that throws leaves
270
352
  * the previous record rather than a claim the target does not meet.
package/src/init/steps.ts CHANGED
@@ -23,7 +23,7 @@ export function buildSteps(
23
23
  {
24
24
  kind: 'run',
25
25
  label: 'Base tooling',
26
- run: child(['tooling', 'sync', 'base', resolved]),
26
+ run: child(['tooling', 'sync', 'base', resolved, '--write']),
27
27
  },
28
28
  {
29
29
  kind: 'run',
@@ -5,9 +5,12 @@ import path from 'path'
5
5
 
6
6
  const portOffset = Number(process.env.WORKTREE_PORT_OFFSET) || 0
7
7
 
8
+ // Reserved TLD, so an unset ASTRO_SITE is visible in canonical URLs and never resolves.
9
+ const site = process.env.ASTRO_SITE || 'https://set-astro-site.invalid'
10
+
8
11
  export default defineConfig({
9
12
  integrations: [react()],
10
- site: process.env.ASTRO_SITE,
13
+ site,
11
14
  server: {
12
15
  port: 4321 + portOffset,
13
16
  },
@@ -10,8 +10,8 @@ The astro stack covers Astro + TypeScript projects: content sites, marketing sit
10
10
 
11
11
  1. Scaffold with `bunx create-astro@latest`. Choose `TypeScript: Strict`. Skip git init and install.
12
12
  2. Add React integration: `bunx astro add react`. Do not use `astro add tailwind`. That command installs the v3 integration. Tailwind v4 arrives via the web manifest.
13
- 3. Install web tooling: `aitk tooling sync web .`
14
- 4. Install astro adapter: `aitk tooling sync astro .`
13
+ 3. Install web tooling: `aitk tooling sync web . --write`
14
+ 4. Install astro adapter: `aitk tooling sync astro . --write`
15
15
  5. Extend the `ci` and `development` context entries under `.claude/context/` per the web reference's extend sections plus the astro rows below.
16
16
  6. Run `bun run lint:fix` then `bun run check`.
17
17
 
@@ -8,7 +8,7 @@ A repository with several language roots layers this stack once at the root and
8
8
 
9
9
  ## What ships as golden configs
10
10
 
11
- Golden config files live in `tooling/base/configs/` and are copied into the target on `aitk tooling sync base .`. They are the source of truth. The reference covers rationale and tradeoffs. Configs show the concrete setup.
11
+ Golden config files live in `tooling/base/configs/` and are copied into the target on `aitk tooling sync base . --write`, replacing whatever sits at those paths. They are the source of truth. The reference covers rationale and tradeoffs. Configs show the concrete setup.
12
12
 
13
13
  - `.prettierrc`: `semi: false`, `singleQuote: true`, plus a parser override per non-standard extension (`.mdx` to `markdown`).
14
14
  - `.shellcheckrc`: `external-sources=true`. Required for shellcheck to follow `source` directives.
@@ -11,9 +11,9 @@ Configs ship as sidecar files (`ruff.toml`, `mypy.ini`, `pytest.ini`, `.coverage
11
11
  ## Scaffold checklist
12
12
 
13
13
  1. Scaffold with `uv init --app <name>`. This creates `pyproject.toml`, `.python-version` pinned to 3.14, `src/<name>/`, and a starter `main.py`. `uv init` defaults `requires-python` to `>=3.14`, which matches the `.python-version` pin this stack ships.
14
- 2. Seed `package.json` so the base layer's bun-side tools (husky, prettier, cspell, commitlint) have a target to install into: `bun init -y`. Without this `aitk tooling sync` drops base configs but skips the dep install, since `resolve_missing_deps` short-circuits when `package.json` is absent.
15
- 3. Install base tooling: `aitk tooling sync base .`
16
- 4. Install python tooling: `aitk tooling sync python .`
14
+ 2. Seed `package.json` so the base layer's bun-side tools (husky, prettier, cspell, commitlint) have a target to install into: `bun init -y`. Without this the sync drops base configs but skips the dep install, since `resolve_missing_deps` short-circuits when `package.json` is absent.
15
+ 3. Install base tooling: `aitk tooling sync base . --write`
16
+ 4. Install python tooling: `aitk tooling sync python . --write`
17
17
  5. Install Python tooling deps: `uv add --dev ruff mypy pytest pytest-cov`. v1 of this stack does not declare these in `[dependencies.dev]` because manifest injection hardcodes `bun add -D`, which can not install Python packages. Until the injector branches on `runtime`, this step is manual.
18
18
  6. Sync the lockfile and create the venv: `uv sync`.
19
19
  7. Annotate `main()` in the scaffold-generated `main.py` with `-> None`. `uv init --app` ships an unannotated `main()` that fails strict mypy on the first run.
@@ -9,8 +9,8 @@ The vite-react stack covers Vite + React + TypeScript projects: web apps and Chr
9
9
  ## Scaffold checklist
10
10
 
11
11
  1. Scaffold with `bunx create-vite@latest <name> --template react-ts` (web apps) or `bunx create-crxjs@latest` (Chrome extensions).
12
- 2. Install base and web tooling: `aitk tooling sync web .`
13
- 3. Install vite-react deps and configs: `aitk tooling sync vite-react .`
12
+ 2. Install base and web tooling: `aitk tooling sync web . --write`
13
+ 3. Install vite-react deps and configs: `aitk tooling sync vite-react . --write`
14
14
  4. Extend the `ci` and `development` context entries under `.claude/context/` per the web reference's extend sections plus the vite-react rows below.
15
15
  5. Run `bun run lint:fix` then `bun run check`.
16
16
 
@@ -8,7 +8,7 @@ The web layer covers web-universal tooling shared across Vite + React, Astro, an
8
8
 
9
9
  ## What ships as golden configs
10
10
 
11
- Golden config files live in `tooling/web/configs/` and are copied into the target on `aitk tooling sync web .`. They are the source of truth. The reference covers rationale and tradeoffs. Configs show the concrete setup.
11
+ Golden config files live in `tooling/web/configs/` and are copied into the target on `aitk tooling sync web . --write`, replacing whatever sits at those paths. They are the source of truth. The reference covers rationale and tradeoffs. Configs show the concrete setup.
12
12
 
13
13
  - `eslint.config.js`: flat config with `@eslint/js`, `typescript-eslint`, React hooks, import sort, check-file, vitest rules scoped to test files, `eslint-config-prettier` last.
14
14
  - `src/test/setup.ts`: `@testing-library/jest-dom` import, `cleanup` after each test.