@warpgogol/forge 5.2.0 → 5.2.4

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