@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.
- package/AGENTS.md +15 -2
- package/bin/cli.ts +4 -0
- package/dist/bin/cli.js +4 -0
- package/dist/bin/cli.js.map +1 -1
- package/dist/os/adr/handlers/implement-stamp.d.ts.map +1 -1
- package/dist/os/adr/handlers/implement-stamp.js +32 -2
- package/dist/os/adr/handlers/implement-stamp.js.map +1 -1
- package/dist/os/adr/types.d.ts +2 -1
- package/dist/os/adr/types.d.ts.map +1 -1
- package/dist/os/naming/naming-convention.d.ts.map +1 -1
- package/dist/os/naming/naming-convention.js +2 -0
- package/dist/os/naming/naming-convention.js.map +1 -1
- package/dist/os/queue/handlers/queue-validate.d.ts +4 -0
- package/dist/os/queue/handlers/queue-validate.d.ts.map +1 -0
- package/dist/os/queue/handlers/queue-validate.js +62 -0
- package/dist/os/queue/handlers/queue-validate.js.map +1 -0
- package/dist/os/queue/index.d.ts +5 -0
- package/dist/os/queue/index.d.ts.map +1 -0
- package/dist/os/queue/index.js +18 -0
- package/dist/os/queue/index.js.map +1 -0
- package/dist/os/queue/manifest.d.ts +14 -0
- package/dist/os/queue/manifest.d.ts.map +1 -0
- package/dist/os/queue/manifest.js +110 -0
- package/dist/os/queue/manifest.js.map +1 -0
- package/dist/os/queue/queue.module.d.ts +3 -0
- package/dist/os/queue/queue.module.d.ts.map +1 -0
- package/dist/os/queue/queue.module.js +56 -0
- package/dist/os/queue/queue.module.js.map +1 -0
- package/dist/os/queue/types.d.ts +25 -0
- package/dist/os/queue/types.d.ts.map +1 -0
- package/dist/os/queue/types.js +28 -0
- package/dist/os/queue/types.js.map +1 -0
- package/dist/os/rfc/frontmatter-io.d.ts.map +1 -1
- package/dist/os/rfc/frontmatter-io.js +1 -0
- package/dist/os/rfc/frontmatter-io.js.map +1 -1
- package/dist/os/rfc/handlers/pipeline-status.d.ts +2 -7
- package/dist/os/rfc/handlers/pipeline-status.d.ts.map +1 -1
- package/dist/os/rfc/handlers/pipeline-status.js +15 -57
- package/dist/os/rfc/handlers/pipeline-status.js.map +1 -1
- package/dist/os/rfc/handlers/validate-rules.d.ts.map +1 -1
- package/dist/os/rfc/handlers/validate-rules.js +6 -1
- package/dist/os/rfc/handlers/validate-rules.js.map +1 -1
- package/dist/os/werkstatt/handlers/workspace-deps.d.ts.map +1 -1
- package/dist/os/werkstatt/handlers/workspace-deps.js +4 -3
- package/dist/os/werkstatt/handlers/workspace-deps.js.map +1 -1
- package/dist/src/onboarding/create.d.ts.map +1 -1
- package/dist/src/onboarding/create.js +5 -2
- package/dist/src/onboarding/create.js.map +1 -1
- package/dist/src/onboarding/doctor.d.ts.map +1 -1
- package/dist/src/onboarding/doctor.js +15 -0
- package/dist/src/onboarding/doctor.js.map +1 -1
- package/dist/src/onboarding/init.d.ts +1 -0
- package/dist/src/onboarding/init.d.ts.map +1 -1
- package/dist/src/onboarding/init.js.map +1 -1
- package/dist/src/onboarding/npm-token-check.d.ts +40 -0
- package/dist/src/onboarding/npm-token-check.d.ts.map +1 -0
- package/dist/src/onboarding/npm-token-check.js +104 -0
- package/dist/src/onboarding/npm-token-check.js.map +1 -0
- package/dist/src/onboarding/scaffold-project.d.ts.map +1 -1
- package/dist/src/onboarding/scaffold-project.js +10 -0
- package/dist/src/onboarding/scaffold-project.js.map +1 -1
- package/dist/src/pipeline-status.d.ts +62 -0
- package/dist/src/pipeline-status.d.ts.map +1 -0
- package/dist/src/pipeline-status.js +220 -0
- package/dist/src/pipeline-status.js.map +1 -0
- package/dist/src/profiles/profile-schema.d.ts +2 -0
- package/dist/src/profiles/profile-schema.d.ts.map +1 -1
- package/dist/src/profiles/profile-schema.js +1 -0
- package/dist/src/profiles/profile-schema.js.map +1 -1
- package/dist/src/profiles/stack-profile.d.ts +1 -0
- package/dist/src/profiles/stack-profile.d.ts.map +1 -1
- package/dist/src/profiles/stack-profile.js +2 -0
- package/dist/src/profiles/stack-profile.js.map +1 -1
- package/os/adr/handlers/implement-stamp.ts +34 -2
- package/os/adr/types.ts +3 -1
- package/os/naming/naming-convention.ts +2 -0
- package/os/queue/handlers/queue-validate.ts +83 -0
- package/os/queue/index.ts +19 -0
- package/os/queue/manifest.ts +183 -0
- package/os/queue/queue-validate.test.ts +177 -0
- package/os/queue/queue.module.ts +59 -0
- package/os/queue/types.ts +49 -0
- package/os/rfc/frontmatter-io.ts +1 -0
- package/os/rfc/handlers/pipeline-status.ts +24 -67
- package/os/rfc/handlers/validate-rules.ts +8 -1
- package/os/werkstatt/handlers/workspace-deps.ts +4 -3
- package/package.json +17 -1
- package/profiles/site-workshop.yaml +654 -0
- package/skills/fo/fo-idea-create-rfc/acceptance-criteria-standard.md +1 -1
- package/skills/fo/fo-idea-i-just-want-to-see-the-result/SKILL.md +46 -1
- package/src/onboarding/create.ts +5 -2
- package/src/onboarding/doctor-token-check.test.ts +124 -0
- package/src/onboarding/doctor.ts +17 -0
- package/src/onboarding/init.ts +1 -0
- package/src/onboarding/npm-token-check.ts +140 -0
- package/src/onboarding/scaffold-project.ts +11 -0
- package/src/onboarding/site-workshop-profile.test.ts +84 -0
- package/src/pipeline-status.test.ts +192 -0
- package/src/pipeline-status.ts +319 -0
- package/src/profiles/profile-schema.ts +2 -0
- package/src/profiles/stack-profile.ts +2 -0
- package/src/tests/adr-implement-stamp.test.ts +150 -0
- 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
|
|