Repository navigation
Expand file tree
/
Copy pathMakefile
More file actions
324 lines (292 loc) · 15.8 KB
/
Copy pathMakefile
File metadata and controls
324 lines (292 loc) · 15.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
# dsh-feature-loop — make targets
#
# The container targets all drive docker/docker-compose.yml, which is the one
# compose file in this repo and defines ONE service (`dsh-feature-loop`).
#
# make up build if needed, start detached, print the URL + token
# make down stop and remove the container (sessions survive in the volume)
# make logs follow the log, which is where the token is printed
#
# Two things worth knowing before you run these:
#
# 1. ONEGW_API_KEY must be set for model calls to work. `make up` injects it from
# ~/.dsh/.credentials.yaml when the environment does not have it, so the
# common case is just `make up`. `make up HOST_PORT=3101` moves the host port.
#
# 2. The host port defaults to 3090 and is bound to 127.0.0.1 only. The harness
# refuses to listen on 0.0.0.0 (it would expose remote code execution), and
# compose deliberately matches that posture: reachable from this machine and
# nowhere else. See docker/README.md.
#
# `make help` lists everything.
SHELL := /bin/bash
.DEFAULT_GOAL := help
# ── configuration ──────────────────────────────────────────────────────────
COMPOSE ?= docker compose -f docker/docker-compose.yml
SERVICE ?= dsh-feature-loop
# The loop's own data (run history). Sessions/settings live in `dsh-data` and
# survive `make clean` — only metrics reset. `DATA_VOLUME` keeps the old
# `VOLUME` name working for callers that set it.
VOLUME ?= dsh-fl-data
DATA_VOLUME ?= dsh-data
HOST_PORT ?= 3090
# The HITL approval dashboard's published host port (compose maps it to the
# container's 8100). 3092 sits clear of the protected ports and of 3090/3091.
DASHBOARD_PORT ?= 3092
DSH_HARNESS ?= $(HOME)/work/harvey/freepeak/deepseek-harness
CREDENTIALS ?= $(HOME)/.dsh/.credentials.yaml
# Ports that must never be disturbed: the user's own GUI and the local dev
# servers from the verification docs.
PROTECTED_PORTS := 3081 3097 3099
# How the container was created matters for introspection. `make up` creates it
# via compose, but a container started by hand (`docker run ...`) is invisible to
# `docker compose logs/ps`, so the read-only targets below talk to the Docker
# daemon directly by name. That works for both.
# Compose is used only for build / up / down.
#
# Compose interpolates ${ONEGW_API_KEY:-} at parse time. Export it from the DSH
# credential store when the caller has not already set it, so `make up` works
# without the key being pasted on the command line (and without it landing in
# shell history).
define export_key
@if [ -z "$$ONEGW_API_KEY" ] && [ -f "$(CREDENTIALS)" ]; then \
export ONEGW_API_KEY=$$(sed -n 's/^[[:space:]]*ONEGW_API_KEY:[[:space:]]*\([^[:space:]]*\).*/\1/p' "$(CREDENTIALS)" | head -1); \
fi; \
if [ -z "$$ONEGW_API_KEY" ]; then \
echo " ! ONEGW_API_KEY is not set and not found in $(CREDENTIALS)."; \
echo " The UI will start, but every model call will fail."; \
echo " Pass it with: make up ONEGW_API_KEY=sk-..."; \
fi
endef
# ── help ───────────────────────────────────────────────────────────────────
.PHONY: help
help: ## Show this help
@echo "dsh-feature-loop — make targets"
@echo
@echo " containers (docker/docker-compose.yml, service '$(SERVICE)')"
@grep -hE '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) \
| awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-16s\033[0m %s\n", $$1, $$2}'
@echo
@echo " variables: PROFILE=$(PROFILE) HOST_PORT=$(HOST_PORT) DASHBOARD_PORT=$(DASHBOARD_PORT)"
@echo " SERVICE=$(SERVICE) VOLUME=$(VOLUME) DSH_HARNESS=$(DSH_HARNESS)"
@echo
@echo " protected ports (never touched by these targets): $(PROTECTED_PORTS)"
# ── install into a local DSH profile ─────────────────────────────────────────
#
# The failure this target exists to prevent: a profile that pulls in nothing else
# from the harness installs this plugin, composes it, shows it in the boot graph,
# and then does nothing at all — no gate, no review, no approval. The harness
# packages are OPTIONAL peers and this repo's .npmrc disables peer
# auto-installation, so they simply do not resolve, and the import fails at load
# in a way that is easy to miss. The README warns about it twice; this target
# makes it impossible to install without noticing.
#
# The check is pnpm's virtual-store key: a resolved peer appears under a
# mangled name like `_@deepseek-ai+c_...`, an unresolved one under its real name.
PROFILE ?= fl
.PHONY: install
install: ## Build, add to a DSH profile, and verify the plugin is NOT inert
@echo "==> building"
@$(MAKE) --no-print-directory build
@echo "==> checking the harness peers resolve"
@for pkg in dsh-llm dsh-typert-protocol; do \
if [ ! -d "node_modules/@deepseek-ai/$$pkg" ]; then \
echo " ! @deepseek-ai/$$pkg is absent from node_modules."; \
echo " The plugin would install INERT: it composes, appears in the boot"; \
echo " graph, and does nothing. Depend on a harness bundle (Agent Teams'"; \
echo " profile bundle is the usual one) so the peers resolve. See"; \
echo " docs/KNOWN-ISSUES.md §4."; exit 1; \
fi; \
done
@echo " peers resolve"
@echo "==> adding to profile '$(PROFILE)'"
@dsh plugin --profile $(PROFILE) add -w file:$(CURDIR)
@# Installing is not enough: a plugin composes only when it is listed in the
@# profile's `dsh.profile.bundles`. `dsh plugin add` adds the dependency and
@# nothing else, so without this the package is present, `--dump-config` has
@# none of its rows, and the deployment looks installed while governing
@# nothing — which is exactly the state the `web` profile was in.
@node scripts/add-bundle.mjs "$(HOME)/.dsh/profiles/$(PROFILE)/package.json" '@freepeak/dsh-feature-loop'
@echo "==> verifying composition"
@if dsh --profile $(PROFILE) --dump-config 2>/dev/null | grep -q 'feature-loop'; then \
echo " the 'feature-loop' row composed"; \
else \
echo " ! no 'feature-loop' row in --dump-config — the plugin is not composed."; \
echo " Check the patch row in cordis.patch.yml and the profile's bundles."; exit 1; \
fi
@echo
@echo " installed into profile '$(PROFILE)'"
@echo " start it: dsh --profile $(PROFILE) web"
@echo " then open the Feature Loop page and describe a goal."
.PHONY: uninstall
uninstall: ## Remove the plugin from a DSH profile
@dsh plugin --profile $(PROFILE) remove @freepeak/dsh-feature-loop 2>/dev/null \
|| echo " nothing to remove from '$(PROFILE)'"
# ── container lifecycle ────────────────────────────────────────────────────
.PHONY: build
build: ## Build the plugin with tsdown into lib/
@command -v pnpm >/dev/null 2>&1 && pnpm build || npm run build
.PHONY: image
image: ## Build the container image
@$(export_key); $(COMPOSE) build
.PHONY: up
up: ## Start the container detached, then print the URL and token
@$(export_key); \
DSH_HOST_PORT=$(HOST_PORT) $(COMPOSE) up -d --build; \
echo; \
echo "waiting for the UI to answer on :$(HOST_PORT) ..."; \
for i in $$(seq 1 45); do \
code=$$(curl -s -o /dev/null -m 2 -w '%{http_code}' http://127.0.0.1:$(HOST_PORT)/ || true); \
if [ "$$code" = "401" ] || [ "$$code" = "200" ]; then break; fi; \
sleep 2; \
done; \
echo; \
if [ "$$code" = "401" ] || [ "$$code" = "200" ]; then \
echo " up (HTTP $$code)"; \
$(MAKE) --no-print-directory url; \
$(MAKE) --no-print-directory dashboard; \
else \
echo " ! the UI did not answer on :$(HOST_PORT) (last code: $$code)"; \
echo " logs:"; $(COMPOSE) logs --tail 30; exit 1; \
fi
.PHONY: down
down: ## Stop and remove the container (both volumes survive: sessions AND history)
@$(COMPOSE) down 2>/dev/null || true
@docker rm -f $(SERVICE) >/dev/null 2>&1 || true
@echo " down — sessions in '$(DATA_VOLUME)', history in '$(VOLUME)'"
.PHONY: restart
restart: ## Restart the container (re-reads its environment)
@$(export_key); \
if docker inspect $(SERVICE) >/dev/null 2>&1; then \
docker restart $(SERVICE) >/dev/null && echo " restarted"; \
else \
echo " no container yet — running 'make up'"; $(MAKE) --no-print-directory up; \
fi; \
sleep 8; $(MAKE) --no-print-directory url
.PHONY: stop
stop: ## Stop the container without removing it
@docker stop $(SERVICE) >/dev/null && echo " stopped" || echo " not running"
.PHONY: start
start: ## Start a previously stopped container
@docker start $(SERVICE) >/dev/null && echo " started" || echo " no container — run: make up"
@$(MAKE) --no-print-directory url
.PHONY: logs
logs: ## Follow the container log (the 'dsh web:' line carries the token)
@docker logs -f --tail 40 $(SERVICE)
.PHONY: url
url: ## Print the UI URL including its token
@token=$$(docker logs $(SERVICE) 2>/dev/null | grep -oE 'token=[A-Za-z0-9_-]+' | tail -1 | cut -d= -f2); \
if [ -z "$$token" ]; then \
echo " no token yet — is the container running? try: make logs"; \
else \
echo " open: http://127.0.0.1:$(HOST_PORT)/?token=$$token"; \
echo " (the token rotates on every boot)"; \
fi
.PHONY: dashboard
dashboard: ## Print the approval dashboard URL including its token
@# Mirrors `url`: the container logs ONE `feature-loop dashboard:` line on
@# bind, carrying the generated token. The line's URL is the CONTAINER's
@# view (127.0.0.1:8100); from the host it is the published loopback port.
@line=$$(docker logs $(SERVICE) 2>/dev/null | grep -oE 'feature-loop dashboard: http://[^ ]+' | tail -1); \
token=$$(printf '%s' "$$line" | grep -oE 'token=[A-Za-z0-9_-]+' | cut -d= -f2); \
if [ -z "$$token" ]; then \
echo " no dashboard line yet — dashboard disabled, or the profile was seeded before"; \
echo " it existed (re-seed with: FORCE_REINIT=1 make up), or try: make logs"; \
else \
echo " open: http://127.0.0.1:$(DASHBOARD_PORT)/?token=$$token"; \
echo " (loopback only; the token rotates on every boot)"; \
fi
.PHONY: health
health: ## Show container status and the HTTP probe
@docker ps -a --filter "name=^$(SERVICE)$$" --format ' {{.Names}} {{.Status}} {{.Image}}' || true
@printf " probe http://127.0.0.1:%s/ -> " "$(HOST_PORT)"; \
curl -s -o /dev/null -m 3 -w '%{http_code}\n' http://127.0.0.1:$(HOST_PORT)/ || echo "no answer"
@printf " published binding: "; docker port $(SERVICE) 2>/dev/null || echo "(container not running)"
.PHONY: shell
shell: ## Open a shell inside the running container
@docker exec -it $(SERVICE) sh
# ── destructive ────────────────────────────────────────────────────────────
.PHONY: clean
clean: ## Remove the container AND the loop history (sessions/settings survive)
@echo " this deletes volume '$(VOLUME)': the run history only."
@echo " sessions, settings and the seeded profile in '$(DATA_VOLUME)' survive."
@read -r -p " type 'yes' to continue: " ok; [ "$$ok" = "yes" ] || { echo " aborted"; exit 1; }
@docker rm -f $(SERVICE) >/dev/null 2>&1 || true
@docker volume rm $(VOLUME) >/dev/null 2>&1 || true
@echo " clean (history reset; sessions kept)"
.PHONY: clean-all
clean-all: ## Remove container, history AND sessions/settings (full reset)
@echo " this deletes '$(VOLUME)' (history) and '$(DATA_VOLUME)' (sessions, settings, profile)"
@read -r -p " type 'yes' to continue: " ok; [ "$$ok" = "yes" ] || { echo " aborted"; exit 1; }
@$(COMPOSE) down -v
@echo " clean-all"
.PHONY: rmi
rmi: ## Remove the container image
@docker rmi dsh-feature-loop:local 2>/dev/null && echo " image removed" || echo " no image to remove"
# ── checks (no container required) ─────────────────────────────────────────
.PHONY: check
check: test typecheck ## Run the test suite and the typecheck
@echo " check passed"
.PHONY: test
test: ## Run the unit test suite (no network)
@node --experimental-strip-types --test test/*.test.ts 2>&1 | tail -8
.PHONY: typecheck
typecheck: ## Typecheck src/ (mirrors the CI file list)
@# Compiler selection first, so the reason is visible in the output.
@# Then the CI invocation. --ignoreConfig is required on TypeScript 6: a
@# tsconfig.json is present but files are named on the command line (TS5112),
@# and the explicit list is deliberate — it is the harness-free closure.
@tsc_bin=""; why=""; \
if [ -x node_modules/.bin/tsc ]; then tsc_bin=node_modules/.bin/tsc; why="project tsconfig"; \
elif [ -f "$(DSH_HARNESS)/node_modules/typescript/lib/tsc.js" ]; then \
tsc_bin="node $(DSH_HARNESS)/node_modules/typescript/lib/tsc.js"; why="harness checkout"; \
elif command -v tsc >/dev/null 2>&1; then tsc_bin=tsc; why="PATH"; fi; \
if [ -z "$$tsc_bin" ]; then echo " ! no TypeScript compiler found. Run: pnpm install"; exit 1; fi; \
echo " compiler: $$why"; \
if [ "$$why" = "project tsconfig" ]; then \
$$tsc_bin --noEmit && echo " typecheck clean ($$why)"; \
else \
roots=""; \
if [ -d "$(HOME)/node_modules/@types" ]; then \
roots="--typeRoots $(HOME)/node_modules/@types --types node"; \
fi; \
$$tsc_bin --noEmit --ignoreConfig \
--target ES2024 --module NodeNext --moduleResolution NodeNext \
--strict --esModuleInterop --skipLibCheck --isolatedModules \
--allowImportingTsExtensions $$roots \
$(CI_FILES) && echo " typecheck clean (CI file list)"; \
fi
# The harness-free import closure, exactly as CI lists it.
CI_FILES := src/agent-policy.ts src/budget.ts src/dashboard.ts \
src/dashboard-page.ts src/envelope.ts src/evidence.ts src/explainer.ts \
src/judge.ts src/laya.ts src/llm.ts src/messages.ts src/metrics.ts \
src/optimize.ts src/optimizer.ts src/phases.ts src/phase-budget.ts \
src/pipeline.ts src/sandbox.ts src/ship.ts src/yolo.ts \
src/observation.ts src/driver.ts src/workers.ts src/worker-dispatch.ts src/worker-changes.ts src/phase-notice.ts \
src/brief.ts src/approval-bridge.ts src/prompts.ts src/questioner.ts \
src/refine.ts src/review.ts src/routing.ts src/runlog.ts \
src/runner.ts src/signals.ts src/spec.ts src/tools.ts
.PHONY: integration
integration: ## Run the real-DSH integration spec (needs the harness checkout)
@bash test/integration/run.sh "$(DSH_HARNESS)"
.PHONY: dashboard-bundle
dashboard-bundle: ## Rebuild the vendored assistant-ui bundle into assets/ (commit the result)
@node web/build.mjs
.PHONY: e2e-dashboard
e2e-dashboard: ## Click the real dashboard page in a real browser (needs Playwright + Chromium; opt-in, not part of verify)
@node --experimental-strip-types test/e2e-dashboard.mjs allow && \
node --experimental-strip-types test/e2e-dashboard.mjs reject
.PHONY: compose-check
compose-check: ## Validate the compose file
@$(COMPOSE) config >/dev/null && echo " compose config valid"
.PHONY: verify
verify: compose-check check integration ## Everything CI runs, plus the integration spec
@echo " verify passed"
# ── safety ─────────────────────────────────────────────────────────────────
.PHONY: ports
ports: ## Show which of the protected ports are in use (do not disturb them)
@for p in $(PROTECTED_PORTS) 3090 $(DASHBOARD_PORT); do \
code=$$(curl -s -o /dev/null -m 2 -w '%{http_code}' http://127.0.0.1:$$p/ || true); \
if [ "$$code" = "000" ] || [ -z "$$code" ]; then echo " $$p free"; \
else echo " $$p in use (HTTP $$code)"; fi; \
done