@warpgogol/forge 5.1.2 → 5.2.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.
Files changed (103) hide show
  1. package/AGENTS.md +15 -2
  2. package/bin/cli.ts +4 -0
  3. package/dist/bin/cli.js +4 -0
  4. package/dist/bin/cli.js.map +1 -1
  5. package/dist/os/adr/handlers/implement-stamp.d.ts.map +1 -1
  6. package/dist/os/adr/handlers/implement-stamp.js +32 -2
  7. package/dist/os/adr/handlers/implement-stamp.js.map +1 -1
  8. package/dist/os/adr/types.d.ts +2 -1
  9. package/dist/os/adr/types.d.ts.map +1 -1
  10. package/dist/os/naming/naming-convention.d.ts.map +1 -1
  11. package/dist/os/naming/naming-convention.js +2 -0
  12. package/dist/os/naming/naming-convention.js.map +1 -1
  13. package/dist/os/queue/handlers/queue-validate.d.ts +4 -0
  14. package/dist/os/queue/handlers/queue-validate.d.ts.map +1 -0
  15. package/dist/os/queue/handlers/queue-validate.js +62 -0
  16. package/dist/os/queue/handlers/queue-validate.js.map +1 -0
  17. package/dist/os/queue/index.d.ts +5 -0
  18. package/dist/os/queue/index.d.ts.map +1 -0
  19. package/dist/os/queue/index.js +18 -0
  20. package/dist/os/queue/index.js.map +1 -0
  21. package/dist/os/queue/manifest.d.ts +14 -0
  22. package/dist/os/queue/manifest.d.ts.map +1 -0
  23. package/dist/os/queue/manifest.js +110 -0
  24. package/dist/os/queue/manifest.js.map +1 -0
  25. package/dist/os/queue/queue.module.d.ts +3 -0
  26. package/dist/os/queue/queue.module.d.ts.map +1 -0
  27. package/dist/os/queue/queue.module.js +56 -0
  28. package/dist/os/queue/queue.module.js.map +1 -0
  29. package/dist/os/queue/types.d.ts +25 -0
  30. package/dist/os/queue/types.d.ts.map +1 -0
  31. package/dist/os/queue/types.js +28 -0
  32. package/dist/os/queue/types.js.map +1 -0
  33. package/dist/os/rfc/frontmatter-io.d.ts.map +1 -1
  34. package/dist/os/rfc/frontmatter-io.js +1 -0
  35. package/dist/os/rfc/frontmatter-io.js.map +1 -1
  36. package/dist/os/rfc/handlers/pipeline-status.d.ts +2 -7
  37. package/dist/os/rfc/handlers/pipeline-status.d.ts.map +1 -1
  38. package/dist/os/rfc/handlers/pipeline-status.js +15 -57
  39. package/dist/os/rfc/handlers/pipeline-status.js.map +1 -1
  40. package/dist/os/rfc/handlers/validate-rules.d.ts.map +1 -1
  41. package/dist/os/rfc/handlers/validate-rules.js +6 -1
  42. package/dist/os/rfc/handlers/validate-rules.js.map +1 -1
  43. package/dist/os/werkstatt/handlers/workspace-deps.d.ts.map +1 -1
  44. package/dist/os/werkstatt/handlers/workspace-deps.js +4 -3
  45. package/dist/os/werkstatt/handlers/workspace-deps.js.map +1 -1
  46. package/dist/src/onboarding/create.d.ts.map +1 -1
  47. package/dist/src/onboarding/create.js +5 -2
  48. package/dist/src/onboarding/create.js.map +1 -1
  49. package/dist/src/onboarding/doctor.d.ts.map +1 -1
  50. package/dist/src/onboarding/doctor.js +15 -0
  51. package/dist/src/onboarding/doctor.js.map +1 -1
  52. package/dist/src/onboarding/init.d.ts +1 -0
  53. package/dist/src/onboarding/init.d.ts.map +1 -1
  54. package/dist/src/onboarding/init.js.map +1 -1
  55. package/dist/src/onboarding/npm-token-check.d.ts +40 -0
  56. package/dist/src/onboarding/npm-token-check.d.ts.map +1 -0
  57. package/dist/src/onboarding/npm-token-check.js +104 -0
  58. package/dist/src/onboarding/npm-token-check.js.map +1 -0
  59. package/dist/src/onboarding/scaffold-project.d.ts.map +1 -1
  60. package/dist/src/onboarding/scaffold-project.js +10 -0
  61. package/dist/src/onboarding/scaffold-project.js.map +1 -1
  62. package/dist/src/pipeline-status.d.ts +62 -0
  63. package/dist/src/pipeline-status.d.ts.map +1 -0
  64. package/dist/src/pipeline-status.js +220 -0
  65. package/dist/src/pipeline-status.js.map +1 -0
  66. package/dist/src/profiles/profile-schema.d.ts +2 -0
  67. package/dist/src/profiles/profile-schema.d.ts.map +1 -1
  68. package/dist/src/profiles/profile-schema.js +1 -0
  69. package/dist/src/profiles/profile-schema.js.map +1 -1
  70. package/dist/src/profiles/stack-profile.d.ts +1 -0
  71. package/dist/src/profiles/stack-profile.d.ts.map +1 -1
  72. package/dist/src/profiles/stack-profile.js +2 -0
  73. package/dist/src/profiles/stack-profile.js.map +1 -1
  74. package/os/adr/handlers/implement-stamp.ts +34 -2
  75. package/os/adr/types.ts +3 -1
  76. package/os/naming/naming-convention.ts +2 -0
  77. package/os/queue/handlers/queue-validate.ts +83 -0
  78. package/os/queue/index.ts +19 -0
  79. package/os/queue/manifest.ts +183 -0
  80. package/os/queue/queue-validate.test.ts +177 -0
  81. package/os/queue/queue.module.ts +59 -0
  82. package/os/queue/types.ts +49 -0
  83. package/os/rfc/frontmatter-io.ts +1 -0
  84. package/os/rfc/handlers/pipeline-status.ts +24 -67
  85. package/os/rfc/handlers/validate-rules.ts +8 -1
  86. package/os/werkstatt/handlers/workspace-deps.ts +4 -3
  87. package/package.json +17 -1
  88. package/profiles/site-workshop.yaml +654 -0
  89. package/skills/fo/fo-idea-create-rfc/acceptance-criteria-standard.md +1 -1
  90. package/skills/fo/fo-idea-i-just-want-to-see-the-result/SKILL.md +46 -1
  91. package/src/onboarding/create.ts +5 -2
  92. package/src/onboarding/doctor-token-check.test.ts +124 -0
  93. package/src/onboarding/doctor.ts +17 -0
  94. package/src/onboarding/init.ts +1 -0
  95. package/src/onboarding/npm-token-check.ts +140 -0
  96. package/src/onboarding/scaffold-project.ts +11 -0
  97. package/src/onboarding/site-workshop-profile.test.ts +84 -0
  98. package/src/pipeline-status.test.ts +192 -0
  99. package/src/pipeline-status.ts +319 -0
  100. package/src/profiles/profile-schema.ts +2 -0
  101. package/src/profiles/stack-profile.ts +2 -0
  102. package/src/tests/adr-implement-stamp.test.ts +150 -0
  103. package/src/tests/profile-schema.test.ts +1 -1
@@ -0,0 +1,654 @@
1
+ schema: forge/stack-profile@1
2
+ id: site-workshop
3
+ displayName: Werkstatt Site Workshop
4
+ detect:
5
+ anyOf:
6
+ - werkstatt.identity.json
7
+ - missions/.gitkeep
8
+ workspace:
9
+ dirs:
10
+ - docs
11
+ - scripts
12
+ - tools
13
+ - missions
14
+ files:
15
+ - path: .vscode/extensions.json
16
+ content: |
17
+ {
18
+ "recommendations": [
19
+ "dbaeumer.vscode-eslint",
20
+ "esbenp.prettier-vscode",
21
+ "github.vscode-github-actions",
22
+ "mechatroner.rainbow-csv"
23
+ ]
24
+ }
25
+ - path: .gitignore
26
+ content: |
27
+ node_modules
28
+ dist
29
+ .turbo
30
+ .cache
31
+ operator-profile.md
32
+ - path: .github/workflows/ci.yml
33
+ content: |
34
+ name: CI
35
+
36
+ on:
37
+ pull_request:
38
+ push:
39
+ branches: [main]
40
+ workflow_dispatch:
41
+
42
+ concurrency:
43
+ group: ${{ github.workflow }}-${{ github.ref }}
44
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
45
+
46
+ permissions:
47
+ contents: read
48
+
49
+ jobs:
50
+ ci:
51
+ runs-on: ubuntu-latest
52
+ timeout-minutes: 15
53
+ name: Lint, type-check, test
54
+ env:
55
+ TZ: UTC
56
+
57
+ steps:
58
+ - name: Checkout
59
+ uses: actions/checkout@v5
60
+
61
+ - name: Enable Corepack
62
+ run: corepack enable
63
+
64
+ - name: Setup Node.js
65
+ uses: actions/setup-node@v5
66
+ with:
67
+ node-version: "24"
68
+ cache: "pnpm"
69
+
70
+ - name: Install dependencies
71
+ run: pnpm install --frozen-lockfile
72
+
73
+ - name: Format check
74
+ run: pnpm format:check
75
+
76
+ - name: Test
77
+ run: pnpm test
78
+
79
+ # Windows CI (optional — uncomment if Windows checks are needed):
80
+ #
81
+ # windows-ci:
82
+ # runs-on: windows-latest
83
+ # name: Lint, type-check, test (Windows)
84
+ # env:
85
+ # GIT_CONFIG_COUNT: 1
86
+ # GIT_CONFIG_KEY_0: core.longpaths
87
+ # GIT_CONFIG_VALUE_0: "true"
88
+ # steps:
89
+ # - name: Checkout
90
+ # uses: actions/checkout@v5
91
+ # - name: Enable Corepack
92
+ # run: corepack enable
93
+ # - name: Setup Node.js
94
+ # uses: actions/setup-node@v5
95
+ # with:
96
+ # node-version: "24"
97
+ # cache: "pnpm"
98
+ # - name: Install dependencies
99
+ # run: pnpm install --frozen-lockfile
100
+ # - name: Format check
101
+ # run: pnpm format:check
102
+ # - name: Test
103
+ # run: pnpm test
104
+ #
105
+ # The GIT_CONFIG_COUNT env block MUST be set before the checkout step
106
+ # so git can handle long file paths during clone on Windows.
107
+ # Do not add Windows to the matrix "just in case" — it requires
108
+ # separate path-length and platform-specific dependency verification.
109
+ - path: package.json
110
+ content: |
111
+ {
112
+ "name": "__PROJECT_NAME__",
113
+ "version": "0.1.0",
114
+ "private": true,
115
+ "type": "module",
116
+ "engines": {
117
+ "node": ">=24 <25"
118
+ },
119
+ "scripts": {
120
+ "clean": "node scripts/clean.mjs",
121
+ "format": "prettier --write .",
122
+ "format:check": "prettier --check .",
123
+ "test": "vitest run --passWithNoTests",
124
+ "upgrade-packages": "pnpm up"
125
+ }
126
+ }
127
+ - path: scripts/clean.mjs
128
+ content: |
129
+ import { readdir, rm } from "node:fs/promises";
130
+ import { join } from "node:path";
131
+ import { fileURLToPath } from "node:url";
132
+
133
+ const TARGETS = new Set(["node_modules", ".turbo", "dist", ".astro", ".cache", ".wrangler"]);
134
+
135
+ const repoRoot = join(fileURLToPath(import.meta.url), "..", "..");
136
+
137
+ async function walk(dir, toDelete) {
138
+ const entries = await readdir(dir, { withFileTypes: true });
139
+ for (const entry of entries) {
140
+ if (!entry.isDirectory()) continue;
141
+ const name = entry.name;
142
+ const fullPath = join(dir, name);
143
+ if (TARGETS.has(name)) {
144
+ toDelete.push(fullPath);
145
+ continue;
146
+ }
147
+ await walk(fullPath, toDelete);
148
+ }
149
+ }
150
+
151
+ const toDelete = [];
152
+ await walk(repoRoot, toDelete);
153
+
154
+ if (toDelete.length === 0) {
155
+ console.log("Nothing to clean.");
156
+ process.exit(0);
157
+ }
158
+
159
+ await Promise.all(toDelete.map((p) => rm(p, { recursive: true, force: true })));
160
+
161
+ for (const p of toDelete) {
162
+ console.log("deleted", p.replace(repoRoot + "/", ""));
163
+ }
164
+
165
+ console.log(`Cleaned ${toDelete.length} directories.`)
166
+ - path: tools/kernel.config.ts
167
+ content: |
168
+ import { defineKernelConfig } from "@warpgogol/werkstatt-engine/kernel/types";
169
+ import { workshopModuleLoaders } from "@warpgogol/werkstatt-engine/kernel/module-map";
170
+
171
+ export default defineKernelConfig({
172
+ name: "__PROJECT_NAME__",
173
+ description: "__PROJECT_NAME__ workshop",
174
+ moduleLoaders: {
175
+ // RFC-1125: shared workshop subset — single-sourced in
176
+ // @warpgogol/werkstatt-engine/kernel/module-map. Do not add loaders here;
177
+ // new workshop-facing modules are added to WORKSHOP_MODULE_MAP once.
178
+ ...workshopModuleLoaders(),
179
+ },
180
+ });
181
+ - path: .npmrc
182
+ content: |
183
+ # @warpgogol scoped registry — replace YOUR_NPM_TOKEN with a valid npm read token
184
+ @warpgogol:registry=https://registry.npmjs.org/
185
+ //registry.npmjs.org/:_authToken=YOUR_NPM_TOKEN
186
+ # Auto-approve build scripts so operators aren't prompted during install
187
+ dangerously-allow-all-builds=true
188
+ - path: eslint.config.js
189
+ content: |
190
+ export default [
191
+ {
192
+ rules: {},
193
+ },
194
+ ];
195
+ - path: .prettierrc.mjs
196
+ content: |
197
+ export default {
198
+ semi: true,
199
+ singleQuote: false,
200
+ trailingComma: "all",
201
+ printWidth: 100,
202
+ tabWidth: 2,
203
+ };
204
+ - path: .gitattributes
205
+ content: |
206
+ # No LFS patterns for this stack
207
+ - path: hooks/pre-commit
208
+ content: |
209
+ #!/bin/sh
210
+ # Platform-scope pre-commit guard
211
+ # Runs forge validation before commits
212
+ echo "Pre-commit: running platform checks..."
213
+ pnpm exec forge run forge.validate || exit 1
214
+ - path: scripts/git-guard.sh
215
+ content: |
216
+ #!/bin/bash
217
+ # git-guard.sh — shell function that intercepts destructive git commands.
218
+ # Sourced by ~/.zshenv (via setup-git-guards.sh).
219
+ # Guards (only active in repos containing scripts/git-guard.sh):
220
+ # git stash, git reset --hard, git checkout --, git checkout -f,
221
+ # git switch -f, git clean -f, git restore
222
+ # Override: ALLOW_DESTRUCTIVE_GIT=1 git stash
223
+
224
+ export FORGE_GIT_GUARD=1
225
+
226
+ _git_guard_find_root() {
227
+ local _repo_root _dir
228
+ _repo_root="$(command git rev-parse --show-toplevel 2>/dev/null || echo "")"
229
+ if [ -z "$_repo_root" ]; then echo ""; return; fi
230
+ if [ -f "$_repo_root/scripts/git-guard.sh" ]; then echo "$_repo_root"; return; fi
231
+ _dir="$(dirname "$_repo_root")"
232
+ while [ -n "$_dir" ] && [ "$_dir" != "/" ]; do
233
+ if [ -f "$_dir/scripts/git-guard.sh" ]; then echo "$_dir"; return; fi
234
+ _dir="$(dirname "$_dir")"
235
+ done
236
+ echo ""
237
+ }
238
+
239
+ git() {
240
+ local _guard_root
241
+ _guard_root="$(_git_guard_find_root)"
242
+ if [ -z "$_guard_root" ] || [ -n "${ALLOW_DESTRUCTIVE_GIT:-}" ]; then
243
+ command git "$@"; return $?
244
+ fi
245
+ local _all_args=("$@") _i=0 _cmd="" _cmd_index=0
246
+ while [ "$_i" -lt "${#_all_args[@]}" ]; do
247
+ local _arg="${_all_args[$_i]}"
248
+ case "$_arg" in
249
+ -C|-c|--git-dir|--work-tree|--namespace) _i=$((_i + 2)); continue ;;
250
+ -*) _i=$((_i + 1)); continue ;;
251
+ esac
252
+ _cmd="$_arg"; _cmd_index="$_i"; break
253
+ done
254
+ local _sub_args=() _j=$((_cmd_index + 1))
255
+ while [ "$_j" -lt "${#_all_args[@]}" ]; do
256
+ _sub_args+=("${_all_args[$_j]}"); _j=$((_j + 1))
257
+ done
258
+ case "$_cmd" in
259
+ stash)
260
+ echo "BLOCKED: git stash is disabled in agent sessions." >&2
261
+ echo "Set ALLOW_DESTRUCTIVE_GIT=1 to override." >&2; return 1 ;;
262
+ reset)
263
+ for _arg in "${_sub_args[@]}"; do
264
+ if [ "$_arg" = "--hard" ] || [[ "$_arg" == --hard=* ]]; then
265
+ echo "BLOCKED: git reset --hard is disabled." >&2; return 1
266
+ fi
267
+ done; command git "$@"; return $? ;;
268
+ checkout)
269
+ for _arg in "${_sub_args[@]}"; do
270
+ if [ "$_arg" = "--" ]; then
271
+ echo "BLOCKED: git checkout -- is disabled." >&2; return 1
272
+ fi
273
+ case "$_arg" in -f*|--force*)
274
+ echo "BLOCKED: git checkout -f is disabled." >&2; return 1 ;;
275
+ esac
276
+ done; command git "$@"; return $? ;;
277
+ switch)
278
+ for _arg in "${_sub_args[@]}"; do
279
+ case "$_arg" in -f*|--force*)
280
+ echo "BLOCKED: git switch -f is disabled." >&2; return 1 ;;
281
+ esac
282
+ done; command git "$@"; return $? ;;
283
+ restore)
284
+ echo "BLOCKED: git restore is disabled." >&2; return 1 ;;
285
+ clean)
286
+ for _arg in "${_sub_args[@]}"; do
287
+ case "$_arg" in -f*|--force*)
288
+ echo "BLOCKED: git clean -f is disabled." >&2; return 1 ;;
289
+ esac
290
+ done; command git "$@"; return $? ;;
291
+ *) command git "$@"; return $? ;;
292
+ esac
293
+ }
294
+ - path: scripts/setup-git-guards.sh
295
+ content: |
296
+ #!/bin/bash
297
+ # setup-git-guards.sh — install shell function guards for destructive git ops.
298
+ # Auto-detects shell: zsh → ~/.zshenv, bash → ~/.bashrc (Git Bash on Windows).
299
+ set -euo pipefail
300
+ repo_root="$(git rev-parse --show-toplevel 2>/dev/null || echo "")"
301
+ if [ -z "$repo_root" ]; then echo "ERROR: not inside a git repository" >&2; exit 1; fi
302
+ guard_script="$repo_root/scripts/git-guard.sh"
303
+ marker="# forge-git-guard"
304
+ # Auto-detect target profile file based on current shell
305
+ case "${SHELL:-}" in
306
+ *zsh*) profile_file="${HOME}/.zshenv" ;;
307
+ *bash*) profile_file="${HOME}/.bashrc" ;;
308
+ *) profile_file="${HOME}/.bashrc" ;;
309
+ esac
310
+ mode="${1:-install}"
311
+ case "$mode" in
312
+ install)
313
+ if [ ! -f "$guard_script" ]; then echo "ERROR: $guard_script not found" >&2; exit 1; fi
314
+ chmod +x "$guard_script" 2>/dev/null || true
315
+ if [ -f "$profile_file" ]; then
316
+ tmp="$(mktemp)"
317
+ awk -v marker="$marker" '$0 == marker { skip=2; next } skip > 0 { skip--; next } { print }' "$profile_file" > "$tmp" || true
318
+ mv "$tmp" "$profile_file" 2>/dev/null || true
319
+ fi
320
+ echo "" >> "$profile_file"
321
+ echo "$marker" >> "$profile_file"
322
+ echo "[ -f \"$guard_script\" ] && source \"$guard_script\"" >> "$profile_file"
323
+ echo "Git guards installed into $profile_file. Restart your shell or run: source $guard_script" ;;
324
+ --verify)
325
+ if [ -z "${FORGE_GIT_GUARD:-}" ]; then echo "MISSING: git guard not loaded" >&2; exit 1; fi
326
+ if ! grep -qF "$marker" "$profile_file" 2>/dev/null; then echo "MISSING: guard not in $profile_file" >&2; exit 1; fi
327
+ echo "OK: git guards installed and loaded" ;;
328
+ --remove)
329
+ if [ -f "$profile_file" ]; then
330
+ tmp="$(mktemp)"
331
+ awk -v marker="$marker" '$0 == marker { skip=2; next } skip > 0 { skip--; next } { print }' "$profile_file" > "$tmp" || true
332
+ mv "$tmp" "$profile_file" 2>/dev/null || true
333
+ fi
334
+ echo "Git guards removed from $profile_file" ;;
335
+ *) echo "Usage: bash scripts/setup-git-guards.sh [install|--verify|--remove]" >&2; exit 1 ;;
336
+ esac
337
+ - path: scripts/clean-stale-stashes.sh
338
+ content: |
339
+ #!/bin/bash
340
+ # clean-stale-stashes.sh — detect and optionally drop stale git stash entries.
341
+ # Usage: bash scripts/clean-stale-stashes.sh [--drop|--drop-all]
342
+ set -euo pipefail
343
+ repo_root="$(git rev-parse --show-toplevel 2>/dev/null || echo "")"
344
+ if [ -z "$repo_root" ]; then echo "ERROR: not inside a git repository" >&2; exit 1; fi
345
+ mode="report"
346
+ if [ "${1:-}" = "--drop" ]; then mode="drop"; elif [ "${1:-}" = "--drop-all" ]; then mode="drop-all"; fi
347
+ stash_count="$(git -C "$repo_root" stash list 2>/dev/null | wc -l)"
348
+ if [ "$stash_count" -eq 0 ]; then exit 0; fi
349
+ stale_indices=() total="$stash_count"
350
+ for ((i = 0; i < total; i++)); do
351
+ ref="stash@{$i}"
352
+ if [ "$mode" = "drop-all" ]; then stale_indices+=("$i"); continue; fi
353
+ tracked_changes="$(git -C "$repo_root" stash show "$ref" --stat 2>/dev/null || true)"
354
+ untracked_files="$(git -C "$repo_root" stash show --include-untracked "$ref" --stat 2>/dev/null || true)"
355
+ if [ -z "$tracked_changes" ] && [ -z "$untracked_files" ]; then stale_indices+=("$i"); continue; fi
356
+ if [ -z "$tracked_changes" ] && [ -n "$untracked_files" ]; then
357
+ all_committed=true
358
+ while IFS= read -r filepath; do
359
+ [ -z "$filepath" ] && continue
360
+ if ! git -C "$repo_root" cat-file -e "HEAD:$filepath" 2>/dev/null; then all_committed=false; break; fi
361
+ done < <(git -C "$repo_root" stash show --include-untracked "$ref" --name-only 2>/dev/null || true)
362
+ if [ "$all_committed" = true ]; then stale_indices+=("$i"); fi
363
+ fi
364
+ done
365
+ if [ ${#stale_indices[@]} -eq 0 ]; then exit 0; fi
366
+ if [ "$mode" = "report" ]; then
367
+ echo "STALE STASH ENTRIES DETECTED (${#stale_indices[@]} of $stash_count):"
368
+ for idx in "${stale_indices[@]}"; do
369
+ echo " stash@{$idx}: $(git -C "$repo_root" stash list | sed -n "$((idx + 1))p")"
370
+ done
371
+ echo "Clean with: bash scripts/clean-stale-stashes.sh --drop"; exit 1
372
+ fi
373
+ if [ "$mode" = "drop-all" ]; then git -C "$repo_root" stash clear; echo "Dropped all $stash_count entries."; exit 0; fi
374
+ for ((i = ${#stale_indices[@]} - 1; i >= 0; i--)); do
375
+ idx="${stale_indices[$i]}"
376
+ git -C "$repo_root" stash drop "stash@{$idx}" 2>/dev/null || true
377
+ done
378
+ echo "Dropped ${#stale_indices[@]} stale entry/entries."; exit 0
379
+ - path: scripts/check-clean-trees.sh
380
+ content: |
381
+ #!/bin/bash
382
+ # check-clean-trees.sh — verify all git trees are clean.
383
+ # Checks repo root + all nested .git directories up to 3 levels deep.
384
+ set -euo pipefail
385
+ repo_root="$(git rev-parse --show-toplevel 2>/dev/null || echo "")"
386
+ if [ -z "$repo_root" ]; then echo "ERROR: not inside a git repository" >&2; exit 1; fi
387
+ dirty=0 output=""
388
+ main_status="$(git -C "$repo_root" status --short 2>/dev/null || true)"
389
+ if [ -n "$main_status" ]; then
390
+ output+="DIRTY: $repo_root\n$(printf '%s\n' "$main_status" | sed 's/^/ /')\n"
391
+ dirty=1
392
+ fi
393
+ while IFS= read -r gitdir; do
394
+ nested_root="$(dirname "$gitdir")"
395
+ [ "$nested_root" = "$repo_root" ] && continue
396
+ nested_status="$(git -C "$nested_root" status --short 2>/dev/null || true)"
397
+ if [ -n "$nested_status" ]; then
398
+ output+="DIRTY: $nested_root\n$(printf '%s\n' "$nested_status" | sed 's/^/ /')\n"
399
+ dirty=1
400
+ fi
401
+ done < <(find "$repo_root" -maxdepth 3 -name ".git" -type d 2>/dev/null | head -20)
402
+ if [ "$dirty" -eq 1 ]; then
403
+ echo -e "$output" | head -40
404
+ echo "---"
405
+ echo "Uncommitted changes detected. Commit before session end."
406
+ exit 1
407
+ fi
408
+ exit 0
409
+ - path: hooks/pre-user-prompt.sh
410
+ content: |
411
+ #!/bin/bash
412
+ # Pre-user-prompt hook: session-end protocol + stale-stash + git-guard checks.
413
+ # Requires jq for JSON payload parsing.
414
+ set -euo pipefail
415
+ payload="$(cat)"
416
+ user_prompt="$(printf '%s' "$payload" | jq -r '.tool_info.user_prompt // empty' 2>/dev/null || true)"
417
+ if [ -z "$user_prompt" ]; then exit 0; fi
418
+ repo_root="$(git rev-parse --show-toplevel 2>/dev/null || echo "")"
419
+ if [ -n "$repo_root" ] && [ -x "$repo_root/scripts/clean-stale-stashes.sh" ]; then
420
+ stash_output="$("$repo_root/scripts/clean-stale-stashes.sh" 2>&1 || true)"
421
+ if [ -n "$stash_output" ]; then
422
+ echo "⚠️ STALE GIT STASH DETECTED" >&2
423
+ echo "$stash_output" >&2
424
+ echo "Clean with: bash scripts/clean-stale-stashes.sh --drop" >&2
425
+ echo "" >&2
426
+ fi
427
+ fi
428
+ if [ -n "$repo_root" ] && [ -f "$repo_root/scripts/git-guard.sh" ]; then
429
+ if [ -z "${FORGE_GIT_GUARD:-}" ]; then
430
+ echo "⚠️ GIT GUARD NOT LOADED — destructive git operations are NOT blocked." >&2
431
+ echo "Run: source $repo_root/scripts/git-guard.sh" >&2
432
+ echo "" >&2
433
+ fi
434
+ fi
435
+ session_end_phrases=("Завершаем эту сессию" "Завершаем сессию" "Заканчиваем сессию" "Завершить сессию" "End session" "Wrap up" "Session end" "/session-end")
436
+ matched=""
437
+ for phrase in "${session_end_phrases[@]}"; do
438
+ if printf '%s' "$user_prompt" | grep -qiF "$phrase"; then matched="$phrase"; break; fi
439
+ done
440
+ if [ -z "$matched" ]; then exit 0; fi
441
+ cat >&2 <<EOF
442
+ SESSION-END PROTOCOL TRIGGERED (matched: "$matched")
443
+ The user's original message was:
444
+ > $user_prompt
445
+ You MUST invoke the fo-session-retro skill via the skill tool BEFORE producing
446
+ any other output. This is a NON-NEGOTIABLE BLOCKED GATE per PREFERENCES.md.
447
+ DO NOT produce a closing summary or ad-hoc output. The closing block must come
448
+ from fo-session-retro's report.
449
+ Protocol steps:
450
+ 1. Verify clean working trees — run: bash scripts/check-clean-trees.sh
451
+ 2. Invoke fo-session-retro via the skill tool
452
+ 3. The retro skill's report IS the session-end output
453
+ EOF
454
+ if [ -n "$repo_root" ] && [ -x "$repo_root/scripts/check-clean-trees.sh" ]; then
455
+ tree_output="$("$repo_root/scripts/check-clean-trees.sh" 2>&1 || true)"
456
+ if [ -n "$tree_output" ]; then echo "" >&2; echo "$tree_output" >&2; fi
457
+ fi
458
+ exit 2
459
+ - path: hooks/pre-user-prompt-wrapper.mjs
460
+ content: |
461
+ #!/usr/bin/env node
462
+ // pre-user-prompt-wrapper.mjs — cross-platform wrapper for pre-user-prompt.sh.
463
+ // Detects bash availability, prints one-time warning if bash not found,
464
+ // then delegates to bash hooks/pre-user-prompt.sh with stdin piped.
465
+ import { execFileSync, spawn } from "node:child_process";
466
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
467
+ import { dirname, join } from "node:path";
468
+ import { fileURLToPath } from "node:url";
469
+
470
+ const __dirname = dirname(fileURLToPath(import.meta.url));
471
+ const scriptPath = join(__dirname, "pre-user-prompt.sh");
472
+ const cacheDir = join(__dirname, "..", ".cache");
473
+ const warningFlag = join(cacheDir, "git-guard-warning-sent");
474
+
475
+ // If pre-user-prompt.sh doesn't exist, silently exit
476
+ if (!existsSync(scriptPath)) {
477
+ process.exit(0);
478
+ }
479
+
480
+ // Check if bash is available
481
+ let bashAvailable = false;
482
+ try {
483
+ execFileSync("bash", ["--version"], { stdio: "ignore" });
484
+ bashAvailable = true;
485
+ } catch {
486
+ bashAvailable = false;
487
+ }
488
+
489
+ if (!bashAvailable) {
490
+ // Print one-time warning if not already sent
491
+ if (!existsSync(warningFlag)) {
492
+ console.error("⚠️ bash not found — pre-user-prompt hook is disabled.");
493
+ console.error("Install Git Bash (Git for Windows) or WSL to enable agent-safety hooks.");
494
+ console.error("");
495
+ try {
496
+ mkdirSync(cacheDir, { recursive: true });
497
+ writeFileSync(warningFlag, new Date().toISOString(), "utf8");
498
+ } catch {
499
+ // Best-effort — don't crash if cache dir is not writable
500
+ }
501
+ }
502
+ process.exit(0);
503
+ }
504
+
505
+ // Delegate to bash script with stdin piped
506
+ const child = spawn("bash", [scriptPath], { stdio: ["inherit", "inherit", "inherit"] });
507
+ child.on("exit", (code) => {
508
+ process.exit(code ?? 0);
509
+ });
510
+ - path: .windsurf/hooks.json
511
+ content: |
512
+ {
513
+ "hooks": {
514
+ "pre_user_prompt": [
515
+ {
516
+ "command": "node $ROOT_WORKSPACE_PATH/hooks/pre-user-prompt-wrapper.mjs",
517
+ "show_output": true
518
+ }
519
+ ]
520
+ }
521
+ }
522
+ - path: missions/.gitkeep
523
+ content: ""
524
+ - path: .forge/pinned.yaml
525
+ content: |
526
+ # Pinned foundation files (DNA-62, RFC-0733)
527
+ # Protect mode: warns on delete/move. Freeze mode: blocks modify too.
528
+ entries:
529
+ - path: tools/kernel.config.ts
530
+ mode: protect
531
+ - path: forge.yaml
532
+ mode: protect
533
+ - path: pnpm-workspace.yaml
534
+ mode: protect
535
+ - path: scripts/git-guard.sh
536
+ mode: protect
537
+ - path: scripts/setup-git-guards.sh
538
+ mode: protect
539
+ - path: hooks/pre-user-prompt.sh
540
+ mode: protect
541
+ - path: hooks/pre-user-prompt-wrapper.mjs
542
+ mode: protect
543
+ - path: README.md
544
+ content: |
545
+ # __PROJECT_NAME__
546
+
547
+ A Werkstatt site workshop — full-lifecycle site management powered by the Warpgogol platform.
548
+
549
+ ## Prerequisites
550
+
551
+ - Node.js 24+
552
+ - pnpm 10+
553
+ - jq (for pre-user-prompt hook)
554
+ - An npm read token for the private `@warpgogol` scope (issued at workshop provisioning)
555
+ - On Windows: Git Bash or WSL (for shell scripts)
556
+
557
+ ## Setup
558
+
559
+ ### 1. Configure the npm token
560
+
561
+ This workshop depends on private @warpgogol packages. Either export the token:
562
+
563
+ ```sh
564
+ export NPM_TOKEN=your_token_here
565
+ ```
566
+
567
+ or edit `.npmrc` and replace `YOUR_NPM_TOKEN` with the token.
568
+
569
+ ### 2. Install dependencies
570
+
571
+ ```sh
572
+ pnpm install
573
+ ```
574
+
575
+ ### 3. Bootstrap workshop identity (required before first site)
576
+
577
+ ```sh
578
+ pnpm exec werkstatt run identity.bootstrap --operator-name <name> --domain <domain>
579
+ ```
580
+
581
+ This creates `werkstatt.identity.json` and prints a private key — store it as `SIGNING_PRIVATE_KEY` in your environment. `sternsystem.register` refuses to run until both exist.
582
+
583
+ ### 4. Register your first site
584
+
585
+ ```sh
586
+ pnpm exec werkstatt run sternsystem.register --id <site-id> --cosmicStar <StarName> --mirrors <cache,bare,...>
587
+ ```
588
+
589
+ ### 5. Verify the workshop
590
+
591
+ ```sh
592
+ pnpm exec forge run doctor
593
+ pnpm exec werkstatt run sternsystem.validate --id <site-id>
594
+ ```
595
+
596
+ ## Stack
597
+
598
+ - Engine: @warpgogol/werkstatt-engine + @warpgogol/werkstatt-site
599
+ - Governance: @warpgogol/forge
600
+ - Stack profile: site-workshop
601
+
602
+ ## Platform updates
603
+
604
+ ```sh
605
+ pnpm exec werkstatt run werkstatt.platform.update --to latest
606
+ ```
607
+
608
+ Bumps `@warpgogol/*` dependencies and reports Sternsystems whose `pinnedPlatform` drifts.
609
+ nextSteps: |
610
+ # Next Steps
611
+
612
+ Your Werkstatt site workshop is ready.
613
+
614
+ ## Step 1: Configure the npm token (required)
615
+
616
+ This workshop installs private `@warpgogol/*` packages. Export the token issued at provisioning:
617
+
618
+ ```sh
619
+ export NPM_TOKEN=your_token_here
620
+ ```
621
+
622
+ or replace `YOUR_NPM_TOKEN` in `.npmrc`. Without it, `pnpm install` fails on registry 404s.
623
+
624
+ ## Step 2: Install dependencies
625
+
626
+ ```sh
627
+ pnpm install
628
+ ```
629
+
630
+ ## Step 3: Bootstrap workshop identity (required before first site)
631
+
632
+ ```sh
633
+ pnpm exec werkstatt run identity.bootstrap --operator-name <name> --domain <domain>
634
+ ```
635
+
636
+ This creates `werkstatt.identity.json` and prints a private key. Store the key as `SIGNING_PRIVATE_KEY` (or `SIGNING_PRIVATE_KEY_PATH`) in your environment — `sternsystem.register` blocks until both the identity file and the signing key exist. An unsigned passport is worse than no registration.
637
+
638
+ ## Step 4: Configure your project
639
+
640
+ Run `/forge-bootstrap` to set language preferences, communication style, and project bindings.
641
+
642
+ ## Step 5: Register a site
643
+
644
+ ```sh
645
+ pnpm exec werkstatt run sternsystem.register --id <site-id> --cosmicStar <StarName> --mirrors <cache,bare,...>
646
+ ```
647
+
648
+ `systems-cache/` is a sibling directory (`../systems-cache/<id>`) — it is created on demand, never inside the workshop root.
649
+
650
+ ## Token lifecycle
651
+
652
+ Per-client granular npm tokens are issued at workshop provisioning and revoked at exit/handover-out. To rotate: replace the token in `.npmrc` or update `NPM_TOKEN`, then re-run `pnpm exec forge run doctor` to verify.
653
+ install:
654
+ - pnpm add -wD @warpgogol/forge @warpgogol/werkstatt-engine @warpgogol/werkstatt-site prettier vitest
@@ -31,7 +31,7 @@ A criterion is a _falsifiable claim about an observable artifact_, not a descrip
31
31
 
32
32
  **Probes.** Every criterion checkable by an existing probe kind MUST get a probe in `acceptance:` frontmatter. Criteria without a feasible probe must state their manual verification procedure explicitly — "manually verified" without a procedure is not evidence.
33
33
 
34
- **Evidence discipline (at check-off time).** `(evidence: ...)` must point to the _mechanism_, not the _artifact_: prefer `probe:AC-N` or `test: path/to/file.test.ts`, then `file:line`. Never check a box based on intention; run the mechanism first.
34
+ **Evidence discipline (at check-off time).** `(evidence: ...)` must point to the _mechanism_, not the _artifact_: prefer `probe:AC-N` or `test: path/to/file.test.ts`, then `file:line`. Never check a box based on intention; run the mechanism first. The annotation is **single-value** (V-37): exactly one of `probe:<AC-id>` (requires a bound probe in `acceptance:` frontmatter), `test:<path>`, or `<path>:<line>` — compound strings like `probe:AC-1; file:…; test:…` fail validation. Pick the strongest single mechanism per criterion.
35
35
 
36
36
  **Self-test before submitting:** hand the criteria set to a hostile reviewer who has _only_ the repo and a shell. If they cannot verify every box without talking to you, the spec is prose, not a spec.
37
37