voidforge-build 23.18.0 → 23.20.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/dist/.claude/agents/celebrimbor-forge-artist.md +1 -0
- package/dist/.claude/agents/ducem-token-economics.md +1 -0
- package/dist/.claude/agents/galadriel-frontend.md +1 -0
- package/dist/.claude/agents/romanoff-integrations.md +4 -0
- package/dist/.claude/agents/silver-surfer-herald.md +19 -4
- package/dist/.claude/commands/architect.md +4 -3
- package/dist/.claude/commands/assemble.md +12 -0
- package/dist/.claude/commands/assess.md +1 -0
- package/dist/.claude/commands/build.md +8 -0
- package/dist/.claude/commands/contextmeter.md +56 -0
- package/dist/.claude/commands/debrief.md +10 -0
- package/dist/.claude/commands/engage.md +5 -0
- package/dist/.claude/commands/git.md +13 -1
- package/dist/.claude/commands/imagine.md +1 -1
- package/dist/.claude/commands/seal.md +80 -0
- package/dist/.claude/commands/ux.md +13 -0
- package/dist/.claude/workflows/assemble-review.workflow.js +26 -6
- package/dist/.claude/workflows/gauntlet.workflow.js +59 -12
- package/dist/CHANGELOG.md +73 -0
- package/dist/CLAUDE.md +9 -1
- package/dist/HOLOCRON.md +16 -2
- package/dist/VERSION.md +3 -1
- package/dist/docs/methods/AI_INTELLIGENCE.md +3 -0
- package/dist/docs/methods/ASSEMBLER.md +12 -0
- package/dist/docs/methods/BUILD_PROTOCOL.md +7 -0
- package/dist/docs/methods/CAMPAIGN.md +11 -0
- package/dist/docs/methods/DEVOPS_ENGINEER.md +56 -0
- package/dist/docs/methods/FIELD_MEDIC.md +1 -0
- package/dist/docs/methods/FORGE_ARTIST.md +3 -4
- package/dist/docs/methods/GAUNTLET.md +6 -0
- package/dist/docs/methods/MUSTER.md +2 -0
- package/dist/docs/methods/PRODUCT_DESIGN_FRONTEND.md +18 -0
- package/dist/docs/methods/QA_ENGINEER.md +17 -1
- package/dist/docs/methods/RELEASE_MANAGER.md +27 -0
- package/dist/docs/methods/SECURITY_AUDITOR.md +11 -1
- package/dist/docs/methods/SUB_AGENTS.md +31 -0
- package/dist/docs/methods/SYSTEMS_ARCHITECT.md +15 -0
- package/dist/docs/methods/TESTING.md +2 -0
- package/dist/docs/methods/TROUBLESHOOTING.md +2 -2
- package/dist/docs/methods/WORKFLOWS.md +18 -2
- package/dist/docs/patterns/ai-prompt-safety.ts +85 -0
- package/dist/docs/patterns/data-pipeline.ts +59 -1
- package/dist/docs/patterns/exclusion-set-invariant.md +62 -0
- package/dist/docs/patterns/multi-tenant-property-test.ts +64 -0
- package/dist/docs/patterns/oauth-token-lifecycle.ts +21 -0
- package/dist/scripts/statusline/README.md +38 -0
- package/dist/scripts/statusline/context-awareness-hook.sh +53 -0
- package/dist/scripts/statusline/settings-snippet.json +17 -0
- package/dist/scripts/statusline/voidforge-statusline.sh +91 -0
- package/dist/scripts/voidforge.js +69 -6
- package/dist/wizard/lib/claude-md-strategy.d.ts +87 -0
- package/dist/wizard/lib/claude-md-strategy.js +198 -0
- package/dist/wizard/lib/marker.d.ts +48 -1
- package/dist/wizard/lib/marker.js +58 -2
- package/dist/wizard/lib/patterns/oauth-token-lifecycle.d.ts +14 -0
- package/dist/wizard/lib/patterns/oauth-token-lifecycle.js +21 -0
- package/dist/wizard/lib/project-init.js +77 -0
- package/dist/wizard/lib/updater.d.ts +19 -0
- package/dist/wizard/lib/updater.js +91 -33
- package/package.json +2 -2
|
@@ -34,6 +34,20 @@ declare const harness: {
|
|
|
34
34
|
listAllReadEndpoints(): string[];
|
|
35
35
|
listAllWriteEndpoints(): string[];
|
|
36
36
|
resetDb(): Promise<void>;
|
|
37
|
+
|
|
38
|
+
// ── Handler-entry (HTTP-level) harness — field report #371 ──────────────
|
|
39
|
+
// Drives the REAL request entrypoint with a concrete credential, so the
|
|
40
|
+
// auth→uid wiring is exercised (not just the repository's WHERE org_id).
|
|
41
|
+
// `principal` is whatever the entrypoint actually authenticates with: a
|
|
42
|
+
// bearer token, a session cookie, an API key header — give two DISTINCT ones.
|
|
43
|
+
httpRequest(
|
|
44
|
+
principal: { headers: Record<string, string> },
|
|
45
|
+
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
|
|
46
|
+
path: string,
|
|
47
|
+
body?: unknown,
|
|
48
|
+
): Promise<{ status: number; json: unknown }>;
|
|
49
|
+
// Two distinct, real principals for the SAME logical resource owner vs other.
|
|
50
|
+
principalForOrg(org: { apiKey: string; userId: string }): { headers: Record<string, string> };
|
|
37
51
|
};
|
|
38
52
|
|
|
39
53
|
// ── The Property ─────────────────────────────────────────────────────────
|
|
@@ -85,6 +99,43 @@ describe('multi-tenant isolation property', () => {
|
|
|
85
99
|
const rowsB = await harness.readAsOrg(orgB, '/api/people');
|
|
86
100
|
expect(rowsB.find((r) => r.org_id === orgA.id)).toBeUndefined();
|
|
87
101
|
});
|
|
102
|
+
|
|
103
|
+
// ── Handler-entry two-principal variant (field report #371) ──────────────
|
|
104
|
+
// The repository-layer property above can pass while a handler that hardcodes
|
|
105
|
+
// `uid = 1` leaks across tenants — the repo test never crosses the auth→uid
|
|
106
|
+
// seam. This variant drives the REAL HTTP entrypoint with TWO DISTINCT
|
|
107
|
+
// credentials and asserts isolation through the request path. It is the test
|
|
108
|
+
// that the planted-bug check below must turn red.
|
|
109
|
+
test('two distinct principals through the real handler do not cross tenants', async () => {
|
|
110
|
+
const orgA = await harness.createOrg();
|
|
111
|
+
const orgB = await harness.createOrg();
|
|
112
|
+
const pA = harness.principalForOrg(orgA);
|
|
113
|
+
const pB = harness.principalForOrg(orgB);
|
|
114
|
+
|
|
115
|
+
// A writes through the real entrypoint with A's own credential.
|
|
116
|
+
const created = await harness.httpRequest(pA, 'POST', '/api/people', { name: 'A-secret' });
|
|
117
|
+
expect(created.status).toBeLessThan(300);
|
|
118
|
+
const writtenId = (created.json as { id: string }).id;
|
|
119
|
+
|
|
120
|
+
// B reads every list endpoint through the real entrypoint with B's credential.
|
|
121
|
+
for (const readEndpoint of harness.listAllReadEndpoints()) {
|
|
122
|
+
const res = await harness.httpRequest(pB, 'GET', readEndpoint);
|
|
123
|
+
const rows = Array.isArray(res.json) ? (res.json as Array<{ id?: string }>) : [];
|
|
124
|
+
expect(rows.find((r) => r.id === writtenId)).toBeUndefined();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Cross-principal direct fetch: B asking for A's row by id must 404, not 403
|
|
128
|
+
// (404 avoids leaking existence — see CLAUDE.md "Return 404, not 403").
|
|
129
|
+
const direct = await harness.httpRequest(pB, 'GET', `/api/people/${writtenId}`);
|
|
130
|
+
expect(direct.status).toBe(404);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// PLANTED-BUG RED-CHECK (field report #371): hardcoding `uid = <owner>` in the
|
|
134
|
+
// handler MUST turn the two-principal test above RED. If you can introduce
|
|
135
|
+
// that bug and the suite stays green, your isolation test is not crossing the
|
|
136
|
+
// auth→uid seam — it is asserting at the repository layer only. Run this once
|
|
137
|
+
// as a mutation check: patch the handler to ignore the authenticated principal
|
|
138
|
+
// and pin uid to org A's id; the test above must fail. Revert after proving it.
|
|
88
139
|
});
|
|
89
140
|
|
|
90
141
|
function randomPayload(): fc.Arbitrary<unknown> {
|
|
@@ -112,8 +163,21 @@ function randomPayload(): fc.Arbitrary<unknown> {
|
|
|
112
163
|
// assert not any(r['id'] == written['id'] for r in rows_b), \
|
|
113
164
|
// f"LEAK: {write_endpoint} -> {read_endpoint}"
|
|
114
165
|
//
|
|
166
|
+
// # Handler-entry two-principal variant (field report #371) — drive the real
|
|
167
|
+
// # entrypoint (FastAPI TestClient / Django test Client) with two distinct
|
|
168
|
+
// # credentials, NOT the repository:
|
|
169
|
+
// # ra = client.post('/api/people', json={'name': 'A'}, headers=princ_a)
|
|
170
|
+
// # rb = client.get(f"/api/people/{ra.json()['id']}", headers=princ_b)
|
|
171
|
+
// # assert rb.status_code == 404 # not 403 — don't leak existence
|
|
172
|
+
// # Mutation check: pin uid=<owner> in the handler; this MUST go red.
|
|
173
|
+
//
|
|
115
174
|
// ── Anti-patterns ────────────────────────────────────────────────────────
|
|
116
175
|
//
|
|
176
|
+
// 0. Asserting isolation only at the repository layer. A handler that
|
|
177
|
+
// hardcodes uid=1 passes every repo-level test while leaking across
|
|
178
|
+
// tenants. The isolation test MUST drive the real request entrypoint with
|
|
179
|
+
// two distinct principals (field report #371). Prove it with the planted
|
|
180
|
+
// uid red-check.
|
|
117
181
|
// 1. Testing isolation only on known endpoints. The bug is in the endpoint
|
|
118
182
|
// you forgot. Property tests enumerate the full surface.
|
|
119
183
|
// 2. Using SUPERUSER fixtures. They silently bypass FORCE RLS at the engine
|
|
@@ -8,6 +8,20 @@
|
|
|
8
8
|
* - Failure escalation: retry 3x → pause platform → alert → requires_reauth
|
|
9
9
|
* - Token stored as encrypted blob in vault, keyed by platform name
|
|
10
10
|
* - Session token (daemon) rotates every 24 hours (§9.19.15)
|
|
11
|
+
* - VERIFY EXPIRY + REFRESH-GRANT BEHAVIOR AGAINST THE PROVIDER'S LIVE DOCS AT
|
|
12
|
+
* INTEGRATION TIME. The PLATFORM_CONFIGS TTLs below are STARTING ASSUMPTIONS,
|
|
13
|
+
* not ground truth — providers change them and "no refresh token / never
|
|
14
|
+
* expires" is a common false assumption. Field report #373: a Todoist
|
|
15
|
+
* integration shipped on "tokens don't expire," but the modern API issues
|
|
16
|
+
* ~1h access tokens WITH a refresh token; the code discarded the refresh
|
|
17
|
+
* token + expiry and registered no refresher, so it died ~1h after every
|
|
18
|
+
* connect across four sessions — looking exactly like intermittent
|
|
19
|
+
* revocation. At integration time: (1) read the provider's OAuth docs and
|
|
20
|
+
* quote the verified access-token TTL + whether a refresh_token is issued;
|
|
21
|
+
* (2) if a refresh_token exists, PERSIST it and register a refresher — never
|
|
22
|
+
* discard it; (3) distinguish "expired" from "revoked" via the API's OWN
|
|
23
|
+
* error body, not by inference (an expired token that mimics revocation will
|
|
24
|
+
* send you reauth-hunting instead of refreshing).
|
|
11
25
|
*
|
|
12
26
|
* Agents: Breeze (platform relations), Dockson (vault)
|
|
13
27
|
*
|
|
@@ -50,6 +64,13 @@ interface PlatformTokenConfig {
|
|
|
50
64
|
revokeEndpoint?: string;
|
|
51
65
|
}
|
|
52
66
|
|
|
67
|
+
// ASSUMPTIONS, NOT GROUND TRUTH (field report #373). These TTLs and the
|
|
68
|
+
// "refreshTokenTtlDays: 0 = never expires" entries are starting points. At
|
|
69
|
+
// integration time, VERIFY each value against the provider's current OAuth
|
|
70
|
+
// docs and the live token response (`expires_in`, presence of `refresh_token`)
|
|
71
|
+
// — a provider that "doesn't expire" today may issue ~1h tokens tomorrow, and
|
|
72
|
+
// a missing refresher then surfaces as recurring prod token-death that mimics
|
|
73
|
+
// revocation. Treat any new platform here the same way before shipping.
|
|
53
74
|
const PLATFORM_CONFIGS: PlatformTokenConfig[] = [
|
|
54
75
|
{ platform: 'meta', accessTokenTtlHours: 1440, refreshTokenTtlDays: 0, refreshEndpoint: 'https://graph.facebook.com/v19.0/oauth/access_token' },
|
|
55
76
|
{ platform: 'google', accessTokenTtlHours: 1, refreshTokenTtlDays: 0, refreshEndpoint: 'https://oauth2.googleapis.com/token' },
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Context Meter — status line + awareness hook
|
|
2
|
+
|
|
3
|
+
Two small scripts that surface how full the context window is — one for the human, one for the model.
|
|
4
|
+
|
|
5
|
+
| Script | Wired to | Audience | What it does |
|
|
6
|
+
|--------|----------|----------|--------------|
|
|
7
|
+
| `voidforge-statusline.sh` | `statusLine` (settings.json) | you | Renders one line: model + a colored meter (`⟦████████░░⟧ 78%`) + tokens remaining. Green → yellow → red as the window fills. |
|
|
8
|
+
| `context-awareness-hook.sh` | `UserPromptSubmit` hook | Claude | Once usage crosses a threshold, injects "you have ~X% left, checkpoint soon" into the model's own context each turn. Silent below the threshold. |
|
|
9
|
+
|
|
10
|
+
The model can't see its own remaining context. The status line tells *you*; the hook tells *Claude* — so it can wrap up open loops and suggest `/vault` or `/seal` before compaction instead of being surprised by it.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
**Default-on.** `npx voidforge-build init` already wires both scripts into a new project's `.claude/settings.json` (warn 80% / crit 92%). Nothing to do for a fresh project.
|
|
15
|
+
|
|
16
|
+
To re-install, retune, or activate on a project that predates this feature, run **`/contextmeter`** — it chmods these scripts and merges the right block into `.claude/settings.json`. Or wire it by hand: merge `settings-snippet.json` into `.claude/settings.json`. Remove with `/contextmeter --uninstall`.
|
|
17
|
+
|
|
18
|
+
## How it reads context
|
|
19
|
+
|
|
20
|
+
- **Status line:** prefers the native `context_window` object Claude Code pipes on stdin (`used_percentage`, `context_window_size`). Falls back to deriving usage from the most recent assistant `message.usage` in `transcript_path` on older Claude Code that doesn't send the field.
|
|
21
|
+
- **Hook:** the hook stdin has no `context_window` object, so it always derives from `transcript_path` (`input_tokens + cache_read_input_tokens + cache_creation_input_tokens`).
|
|
22
|
+
- 1M-token sessions are detected automatically (usage above 200k ⇒ 1,000,000 denominator), or set `VOIDFORGE_CONTEXT_WINDOW`.
|
|
23
|
+
|
|
24
|
+
## Tuning (env)
|
|
25
|
+
|
|
26
|
+
| Var | Default | Effect |
|
|
27
|
+
|-----|---------|--------|
|
|
28
|
+
| `VOIDFORGE_CONTEXT_WINDOW` | `200000` | Denominator when the size field is absent. |
|
|
29
|
+
| `VOIDFORGE_CONTEXT_WARN_PCT` | `80` | Hook starts speaking — and the meter turns yellow — at this % used. |
|
|
30
|
+
| `VOIDFORGE_CONTEXT_CRIT_PCT` | `92` | Hook escalates to "checkpoint NOW" — and the meter turns red — at this %. |
|
|
31
|
+
|
|
32
|
+
Both scripts read the same two thresholds, so the meter's yellow/red bands stay in lockstep with the hook's warn/critical bands. `/contextmeter --warn-pct N` / `--crit-pct N` bake these into the command strings in settings.json so they persist without a shell export.
|
|
33
|
+
|
|
34
|
+
## Requirements & caveats
|
|
35
|
+
|
|
36
|
+
- **`jq`** is required. Without it the status line prints a one-line "install jq" notice and the hook no-ops — neither ever breaks your session.
|
|
37
|
+
- Only the **first line** of status-line stdout is shown by Claude Code, so the meter is deliberately single-line.
|
|
38
|
+
- **Name:** this ships as `/contextmeter`, not `/statusline` — Claude Code's native `/statusline` and `/context` commands always shadow a same-named project command (see `docs/NATIVE_CAPABILITIES.md`).
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# context-awareness-hook.sh — UserPromptSubmit hook that injects context-budget
|
|
3
|
+
# awareness INTO Claude's own context as the window fills.
|
|
4
|
+
#
|
|
5
|
+
# The status-line meter is for the human; this hook is for the model. Claude
|
|
6
|
+
# cannot see its own remaining context directly, so each turn (once usage crosses
|
|
7
|
+
# a threshold) this prints a JSON object whose `hookSpecificOutput.additionalContext`
|
|
8
|
+
# Claude receives — "you have ~X% left, checkpoint soon." Below the threshold it is
|
|
9
|
+
# silent, so it adds zero noise until it matters.
|
|
10
|
+
#
|
|
11
|
+
# Cadence: Claude Code has no time/turn-interval hooks — UserPromptSubmit (once per
|
|
12
|
+
# user turn) is the finest cadence available, which is exactly when fresh awareness
|
|
13
|
+
# is useful. Threshold-gated so it behaves like a periodic warning that only speaks
|
|
14
|
+
# near the limit.
|
|
15
|
+
#
|
|
16
|
+
# Requires jq; without it, no-op (exit 0). A hook must never break the turn.
|
|
17
|
+
#
|
|
18
|
+
# Env knobs:
|
|
19
|
+
# VOIDFORGE_CONTEXT_WINDOW denominator (default 200000; auto-bumps to 1000000 when usage exceeds 200k)
|
|
20
|
+
# VOIDFORGE_CONTEXT_WARN_PCT start warning at this % used (default 80)
|
|
21
|
+
# VOIDFORGE_CONTEXT_CRIT_PCT escalate to "checkpoint NOW" at this % (default 92)
|
|
22
|
+
set -uo pipefail
|
|
23
|
+
|
|
24
|
+
input="$(cat 2>/dev/null || true)"
|
|
25
|
+
command -v jq >/dev/null 2>&1 || exit 0
|
|
26
|
+
|
|
27
|
+
transcript="$(printf '%s' "$input" | jq -r '.transcript_path // empty' 2>/dev/null)"
|
|
28
|
+
[ -n "$transcript" ] && [ -f "$transcript" ] || exit 0
|
|
29
|
+
|
|
30
|
+
usage="$(tail -n 400 "$transcript" | jq -c 'select(.message.usage != null) | .message.usage' 2>/dev/null | tail -1)"
|
|
31
|
+
[ -n "$usage" ] || exit 0
|
|
32
|
+
used="$(printf '%s' "$usage" | jq -r '((.input_tokens//0)+(.cache_read_input_tokens//0)+(.cache_creation_input_tokens//0))' 2>/dev/null)"
|
|
33
|
+
used="${used%%.*}"
|
|
34
|
+
[ -n "${used:-}" ] || exit 0
|
|
35
|
+
|
|
36
|
+
if [ "$used" -gt 200000 ] 2>/dev/null; then window=1000000; else window="${VOIDFORGE_CONTEXT_WINDOW:-200000}"; fi
|
|
37
|
+
[ "${window:-0}" -gt 0 ] 2>/dev/null || exit 0
|
|
38
|
+
pct=$(( used * 100 / window ))
|
|
39
|
+
|
|
40
|
+
warn="${VOIDFORGE_CONTEXT_WARN_PCT:-80}"
|
|
41
|
+
crit="${VOIDFORGE_CONTEXT_CRIT_PCT:-92}"
|
|
42
|
+
[ "$pct" -lt "$warn" ] && exit 0
|
|
43
|
+
|
|
44
|
+
rem_k=$(( (window - used) / 1000 ))
|
|
45
|
+
|
|
46
|
+
if [ "$pct" -ge "$crit" ]; then
|
|
47
|
+
msg="⚠️ CONTEXT CRITICAL: ~${pct}% of the ${window}-token window is used (~${rem_k}k left). Compaction is imminent — checkpoint NOW: run /vault (or /seal) to preserve session state before the context is summarized, and prefer finishing the current sub-task over starting new work."
|
|
48
|
+
else
|
|
49
|
+
msg="Context monitor: ~${pct}% of the ${window}-token window is used (~${rem_k}k left). You are approaching the limit — wrap up open loops and consider /vault or /seal to checkpoint before compaction."
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
jq -cn --arg m "$msg" '{hookSpecificOutput:{hookEventName:"UserPromptSubmit",additionalContext:$m}}'
|
|
53
|
+
exit 0
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"statusLine": {
|
|
3
|
+
"type": "command",
|
|
4
|
+
"command": "bash scripts/statusline/voidforge-statusline.sh",
|
|
5
|
+
"padding": 0
|
|
6
|
+
},
|
|
7
|
+
"hooks": {
|
|
8
|
+
"UserPromptSubmit": [
|
|
9
|
+
{
|
|
10
|
+
"matcher": "",
|
|
11
|
+
"hooks": [
|
|
12
|
+
{ "type": "command", "command": "bash scripts/statusline/context-awareness-hook.sh" }
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# voidforge-statusline.sh — Context-usage meter for the Claude Code status line.
|
|
3
|
+
#
|
|
4
|
+
# Reads the status-line JSON on stdin and prints ONE line:
|
|
5
|
+
# <model> ⟦████████░░⟧ 78% ctx · 44k left
|
|
6
|
+
# The meter is colored green → yellow → red as the context window fills.
|
|
7
|
+
#
|
|
8
|
+
# Source of truth: the native `.context_window` object Claude Code pipes to the
|
|
9
|
+
# status line (`used_percentage`, `context_window_size`). When that field is
|
|
10
|
+
# absent (older Claude Code), it falls back to deriving usage from the most
|
|
11
|
+
# recent assistant `message.usage` in `.transcript_path`.
|
|
12
|
+
#
|
|
13
|
+
# Requires jq. Without jq it prints a minimal line and exits 0 — a status line
|
|
14
|
+
# must NEVER hard-fail (that would blank the bar).
|
|
15
|
+
#
|
|
16
|
+
# Env knobs (shared with the awareness hook so colors and warnings stay in lockstep):
|
|
17
|
+
# VOIDFORGE_CONTEXT_WINDOW denominator when the size field is absent (default 200000)
|
|
18
|
+
# VOIDFORGE_CONTEXT_WARN_PCT meter turns yellow at this % used (default 80)
|
|
19
|
+
# VOIDFORGE_CONTEXT_CRIT_PCT meter turns red at this % used (default 92)
|
|
20
|
+
set -uo pipefail
|
|
21
|
+
|
|
22
|
+
input="$(cat 2>/dev/null || true)"
|
|
23
|
+
|
|
24
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
25
|
+
printf 'VoidForge · ctx meter needs jq (brew install jq)\n'
|
|
26
|
+
exit 0
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
j() { printf '%s' "$input" | jq -r "$1" 2>/dev/null; }
|
|
30
|
+
|
|
31
|
+
model="$(j '.model.display_name // .model.id // "Claude"')"
|
|
32
|
+
pct="$(j '.context_window.used_percentage // empty')"
|
|
33
|
+
window="$(j '.context_window.context_window_size // empty')"
|
|
34
|
+
|
|
35
|
+
# Fallback: derive from the transcript when the native field is absent.
|
|
36
|
+
if [ -z "$pct" ]; then
|
|
37
|
+
transcript="$(j '.transcript_path // empty')"
|
|
38
|
+
if [ -n "$transcript" ] && [ -f "$transcript" ]; then
|
|
39
|
+
usage="$(tail -n 400 "$transcript" | jq -c 'select(.message.usage != null) | .message.usage' 2>/dev/null | tail -1)"
|
|
40
|
+
if [ -n "$usage" ]; then
|
|
41
|
+
used="$(printf '%s' "$usage" | jq -r '((.input_tokens//0)+(.cache_read_input_tokens//0)+(.cache_creation_input_tokens//0))' 2>/dev/null)"
|
|
42
|
+
used="${used%%.*}"
|
|
43
|
+
if [ -z "$window" ]; then
|
|
44
|
+
if [ "${used:-0}" -gt 200000 ] 2>/dev/null; then window=1000000; else window="${VOIDFORGE_CONTEXT_WINDOW:-200000}"; fi
|
|
45
|
+
fi
|
|
46
|
+
if [ -n "${used:-}" ] && [ "${window:-0}" -gt 0 ] 2>/dev/null; then
|
|
47
|
+
pct=$(( used * 100 / window ))
|
|
48
|
+
fi
|
|
49
|
+
fi
|
|
50
|
+
fi
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
# Coerce to integer; bail to model-only if we still have nothing.
|
|
54
|
+
pct="${pct%%.*}"
|
|
55
|
+
if [ -z "$pct" ]; then
|
|
56
|
+
printf '%s\n' "$model"
|
|
57
|
+
exit 0
|
|
58
|
+
fi
|
|
59
|
+
[ -z "$window" ] && window="${VOIDFORGE_CONTEXT_WINDOW:-200000}"
|
|
60
|
+
window="${window%%.*}"
|
|
61
|
+
|
|
62
|
+
[ "$pct" -lt 0 ] 2>/dev/null && pct=0
|
|
63
|
+
[ "$pct" -gt 100 ] 2>/dev/null && pct=100
|
|
64
|
+
|
|
65
|
+
remaining=$(( window - window * pct / 100 ))
|
|
66
|
+
if [ "$remaining" -ge 1000 ]; then rem_h="$(( remaining / 1000 ))k"; else rem_h="${remaining}"; fi
|
|
67
|
+
|
|
68
|
+
# Color band — defaults align with the awareness-hook thresholds (warn 80 → yellow,
|
|
69
|
+
# crit 92 → red) so the meter turns red exactly when the hook goes critical. Both
|
|
70
|
+
# honor the same env vars, so retuning one retunes the other.
|
|
71
|
+
yellow_at="${VOIDFORGE_CONTEXT_WARN_PCT:-80}"
|
|
72
|
+
red_at="${VOIDFORGE_CONTEXT_CRIT_PCT:-92}"
|
|
73
|
+
if [ "$pct" -ge "$red_at" ]; then color=$'\033[31m' # red — checkpoint now
|
|
74
|
+
elif [ "$pct" -ge "$yellow_at" ]; then color=$'\033[33m' # yellow — getting full
|
|
75
|
+
else color=$'\033[32m' # green — healthy
|
|
76
|
+
fi
|
|
77
|
+
reset=$'\033[0m'
|
|
78
|
+
dim=$'\033[2m'
|
|
79
|
+
|
|
80
|
+
# 10-cell meter, rounded.
|
|
81
|
+
filled=$(( (pct + 5) / 10 ))
|
|
82
|
+
[ "$filled" -gt 10 ] && filled=10
|
|
83
|
+
[ "$filled" -lt 0 ] && filled=0
|
|
84
|
+
bar=""
|
|
85
|
+
i=0
|
|
86
|
+
while [ "$i" -lt 10 ]; do
|
|
87
|
+
if [ "$i" -lt "$filled" ]; then bar="${bar}█"; else bar="${bar}░"; fi
|
|
88
|
+
i=$(( i + 1 ))
|
|
89
|
+
done
|
|
90
|
+
|
|
91
|
+
printf '%s %s⟦%s⟧ %d%%%s %sctx · %s left%s\n' "$model" "$color" "$bar" "$pct" "$reset" "$dim" "$rem_h" "$reset"
|
|
@@ -345,6 +345,21 @@ async function cmdMigrateTreasury() {
|
|
|
345
345
|
console.log(' Per-project logs start fresh (genesis hash). Global data preserved in archive.');
|
|
346
346
|
console.log(' To rollback: move archive back to ~/.voidforge/treasury/\n');
|
|
347
347
|
}
|
|
348
|
+
function showUpdateHelp() {
|
|
349
|
+
console.log('voidforge update — update project methodology (Bombadil)\n');
|
|
350
|
+
console.log('Usage: npx voidforge update [options]\n');
|
|
351
|
+
console.log('Options:');
|
|
352
|
+
console.log(' --self Update the wizard/CLI itself instead of the project');
|
|
353
|
+
console.log(' --extensions Update all installed extensions across registered projects');
|
|
354
|
+
console.log(' --no-self-update Skip the automatic CLI self-upgrade check');
|
|
355
|
+
console.log(' --help, -h Show this help (does NOT run the update)');
|
|
356
|
+
console.log('');
|
|
357
|
+
console.log('CLAUDE.md is updated per the `.voidforge` marker `claudeMd` policy:');
|
|
358
|
+
console.log(' preserve (default) Never overwrite in place — write CLAUDE.md.upstream + warn');
|
|
359
|
+
console.log(' merge Replace only the VOIDFORGE methodology fences, keep project sections');
|
|
360
|
+
console.log(' skip Never touch CLAUDE.md');
|
|
361
|
+
console.log('');
|
|
362
|
+
}
|
|
348
363
|
function showHelp() {
|
|
349
364
|
console.log('VoidForge — From nothing, everything.\n');
|
|
350
365
|
console.log('Usage: npx voidforge <command> [options]\n');
|
|
@@ -464,7 +479,16 @@ async function main() {
|
|
|
464
479
|
break;
|
|
465
480
|
}
|
|
466
481
|
case 'update': {
|
|
467
|
-
|
|
482
|
+
const { resolveUpdateMode } = await import('../wizard/lib/updater.js');
|
|
483
|
+
const updateMode = resolveUpdateMode(args);
|
|
484
|
+
// Help guard (issue #368): `update --help` / `-h` must PRINT usage and
|
|
485
|
+
// exit — never execute the (potentially destructive) update. Help wins
|
|
486
|
+
// over every action flag, checked before any work happens.
|
|
487
|
+
if (updateMode === 'help') {
|
|
488
|
+
showUpdateHelp();
|
|
489
|
+
break;
|
|
490
|
+
}
|
|
491
|
+
if (updateMode === 'self') {
|
|
468
492
|
const { selfUpdate } = await import('../wizard/lib/updater.js');
|
|
469
493
|
const result = selfUpdate();
|
|
470
494
|
console.log(result.message);
|
|
@@ -510,7 +534,7 @@ async function main() {
|
|
|
510
534
|
// npm view failed — offline or registry issue. Continue with local version.
|
|
511
535
|
}
|
|
512
536
|
}
|
|
513
|
-
if (
|
|
537
|
+
if (updateMode === 'extensions') {
|
|
514
538
|
const { readRegistry } = await import('../wizard/lib/project-registry.js');
|
|
515
539
|
const { readMarker: readMkr } = await import('../wizard/lib/marker.js');
|
|
516
540
|
const { getExtension } = await import('../wizard/lib/extensions.js');
|
|
@@ -537,11 +561,42 @@ async function main() {
|
|
|
537
561
|
break;
|
|
538
562
|
}
|
|
539
563
|
// Methodology update
|
|
540
|
-
const { findProjectRoot: findProjRoot } = await import('../wizard/lib/marker.js');
|
|
541
|
-
|
|
564
|
+
const { findProjectRoot: findProjRoot, detectLegacyConsumer, readVersionFile, createMarker: makeMarker, writeMarker: saveMarker, } = await import('../wizard/lib/marker.js');
|
|
565
|
+
let projRoot = findProjRoot();
|
|
542
566
|
if (!projRoot) {
|
|
543
|
-
|
|
544
|
-
|
|
567
|
+
// Issue #369: a project that consumed methodology via git (pre-marker)
|
|
568
|
+
// has no `.voidforge` but IS a real consumer. Offer to create the
|
|
569
|
+
// marker instead of sending the user to `init` (which scaffolds).
|
|
570
|
+
const legacy = detectLegacyConsumer();
|
|
571
|
+
if (legacy) {
|
|
572
|
+
const version = readVersionFile(legacy.dir);
|
|
573
|
+
console.log('\n No .voidforge marker found, but this looks like a legacy');
|
|
574
|
+
console.log(' VoidForge methodology consumer:');
|
|
575
|
+
console.log(` dir: ${legacy.dir}`);
|
|
576
|
+
console.log(` tier: ${legacy.inferredTier} (inferred)`);
|
|
577
|
+
console.log(` version: ${version} (from VERSION.md)`);
|
|
578
|
+
console.log('');
|
|
579
|
+
let createIt = true;
|
|
580
|
+
if (process.stdin.isTTY) {
|
|
581
|
+
const answer = (await prompt(' Create the .voidforge marker now? [Y/n]: ')).toLowerCase();
|
|
582
|
+
createIt = answer === '' || answer === 'y' || answer === 'yes';
|
|
583
|
+
}
|
|
584
|
+
else {
|
|
585
|
+
console.log(' Non-interactive: creating the marker automatically.');
|
|
586
|
+
}
|
|
587
|
+
if (!createIt) {
|
|
588
|
+
console.log('\n Aborted — no marker created. Update skipped.\n');
|
|
589
|
+
break;
|
|
590
|
+
}
|
|
591
|
+
const newMarker = makeMarker(version, legacy.inferredTier);
|
|
592
|
+
await saveMarker(legacy.dir, newMarker);
|
|
593
|
+
console.log(` Created .voidforge marker (${newMarker.id}).\n`);
|
|
594
|
+
projRoot = legacy.dir;
|
|
595
|
+
}
|
|
596
|
+
else {
|
|
597
|
+
console.error('Not a VoidForge project — run `npx voidforge init` first.');
|
|
598
|
+
process.exit(1);
|
|
599
|
+
}
|
|
545
600
|
}
|
|
546
601
|
const { diffMethodology, applyUpdate } = await import('../wizard/lib/updater.js');
|
|
547
602
|
const plan = await diffMethodology(projRoot);
|
|
@@ -566,6 +621,14 @@ async function main() {
|
|
|
566
621
|
console.log(` - ${f} (kept locally)`);
|
|
567
622
|
}
|
|
568
623
|
console.log(` Unchanged: ${plan.unchanged} files\n`);
|
|
624
|
+
// CLAUDE.md non-destructive handling (issue #368) — surface the policy,
|
|
625
|
+
// any dropped-section warning, and the side-file path before applying.
|
|
626
|
+
if (plan.claudeMd && plan.claudeMd.warnings.length > 0) {
|
|
627
|
+
console.log(' CLAUDE.md:');
|
|
628
|
+
for (const w of plan.claudeMd.warnings)
|
|
629
|
+
console.log(` ${w}`);
|
|
630
|
+
console.log('');
|
|
631
|
+
}
|
|
569
632
|
const result = await applyUpdate(projRoot);
|
|
570
633
|
console.log(` Updated to v${result.newVersion}. ${plan.added.length + plan.modified.length} files changed.\n`);
|
|
571
634
|
break;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLAUDE.md update strategy — the single mechanism the updater uses to decide
|
|
3
|
+
* how an `update` may touch a project's CLAUDE.md (issue #368).
|
|
4
|
+
*
|
|
5
|
+
* CLAUDE.md is the file Claude Code loads every session; it carries the
|
|
6
|
+
* project's operational knowledge. The old updater preserved only the first ~10
|
|
7
|
+
* lines and overwrote the rest, silently discarding every project-specific
|
|
8
|
+
* section. That is the same bug class as #331 (silent destruction of user
|
|
9
|
+
* content). This module makes the update NON-DESTRUCTIVE by default and gives
|
|
10
|
+
* projects a precise, opt-in lossless merge via sentinel fences.
|
|
11
|
+
*
|
|
12
|
+
* Three strategies (from the `.voidforge` marker's `claudeMd` field):
|
|
13
|
+
* - 'preserve' (default): never overwrite in place. If upstream differs, the
|
|
14
|
+
* new methodology is written to a side file (`CLAUDE.md.upstream`) and the
|
|
15
|
+
* operator is warned. The original CLAUDE.md is left untouched.
|
|
16
|
+
* - 'merge': replace ONLY the content between the sentinel fences
|
|
17
|
+
* `<!-- VOIDFORGE:BEGIN methodology -->` / `<!-- VOIDFORGE:END methodology -->`,
|
|
18
|
+
* leaving everything outside them verbatim. Falls back to 'preserve'
|
|
19
|
+
* (side-file + warning) when the fences are absent in either document —
|
|
20
|
+
* there is no lossless in-place merge without an explicit fenced block.
|
|
21
|
+
* - 'skip': do not read or write CLAUDE.md at all.
|
|
22
|
+
*
|
|
23
|
+
* In every strategy, section-loss detection runs and surfaces a warning so an
|
|
24
|
+
* update can never silently drop project sections.
|
|
25
|
+
*/
|
|
26
|
+
import type { ClaudeMdStrategy } from './marker.js';
|
|
27
|
+
export declare const FENCE_BEGIN = "<!-- VOIDFORGE:BEGIN methodology -->";
|
|
28
|
+
export declare const FENCE_END = "<!-- VOIDFORGE:END methodology -->";
|
|
29
|
+
/** Side file written when an in-place merge would be destructive. */
|
|
30
|
+
export declare const UPSTREAM_SUFFIX = ".upstream";
|
|
31
|
+
export type ClaudeMdAction = 'unchanged' | 'skip' | 'overwrite' | 'merge-fenced' | 'side-file';
|
|
32
|
+
export interface ClaudeMdMergeResult {
|
|
33
|
+
action: ClaudeMdAction;
|
|
34
|
+
/**
|
|
35
|
+
* New content to write to CLAUDE.md itself, or null if CLAUDE.md must NOT be
|
|
36
|
+
* touched (skip / unchanged / side-file paths).
|
|
37
|
+
*/
|
|
38
|
+
claudeMdContent: string | null;
|
|
39
|
+
/**
|
|
40
|
+
* Content to write to the side file, or null if no side file is needed.
|
|
41
|
+
* When set, the caller writes it to `<CLAUDE.md path>.upstream`.
|
|
42
|
+
*/
|
|
43
|
+
sideFileContent: string | null;
|
|
44
|
+
/** Project headings present locally but absent upstream — would be dropped by a naive overwrite. */
|
|
45
|
+
droppedSections: string[];
|
|
46
|
+
/** Human-readable warnings to surface to the operator. */
|
|
47
|
+
warnings: string[];
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Extract top-level-ish markdown headings (#, ##) used as section identities.
|
|
51
|
+
* Code fences are skipped so a `#` inside a ```bash block is not a heading.
|
|
52
|
+
* Normalized (trimmed, leading hashes stripped) for stable comparison.
|
|
53
|
+
*/
|
|
54
|
+
export declare function extractSections(content: string): string[];
|
|
55
|
+
/**
|
|
56
|
+
* Sections present in `current` but missing from `incoming` — i.e. content that
|
|
57
|
+
* a naive whole-file overwrite would silently destroy.
|
|
58
|
+
*/
|
|
59
|
+
export declare function findDroppedSections(current: string, incoming: string): string[];
|
|
60
|
+
/**
|
|
61
|
+
* Normalize away the project-identity region so two CLAUDE.md files that differ
|
|
62
|
+
* ONLY in their `## Project` block (name/one-liner/domain/repo, or the
|
|
63
|
+
* `[PROJECT_NAME]` placeholders) compare equal.
|
|
64
|
+
*
|
|
65
|
+
* This is why a freshly-`init`ed project — whose `## Project` block has the real
|
|
66
|
+
* name injected while the upstream template still carries `[PROJECT_NAME]` —
|
|
67
|
+
* does not register as a spurious "change" on the very next `update`. It is a
|
|
68
|
+
* comparison-only transform; we never write the normalized form.
|
|
69
|
+
*/
|
|
70
|
+
export declare function stripIdentity(content: string): string;
|
|
71
|
+
export declare function hasFences(content: string): boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Replace the fenced methodology block in `current` with the fenced block from
|
|
74
|
+
* `upstream`. Everything outside the fences in `current` is preserved verbatim.
|
|
75
|
+
* Returns null if either document lacks a well-formed fence pair.
|
|
76
|
+
*/
|
|
77
|
+
export declare function mergeFenced(current: string, upstream: string): string | null;
|
|
78
|
+
/**
|
|
79
|
+
* Decide how to update CLAUDE.md given the current project content, the incoming
|
|
80
|
+
* upstream content, and the configured strategy. Pure function — performs no
|
|
81
|
+
* I/O. The caller performs the writes described by the returned result.
|
|
82
|
+
*
|
|
83
|
+
* @param current Existing project CLAUDE.md (null/undefined if the file is absent).
|
|
84
|
+
* @param upstream Incoming methodology CLAUDE.md.
|
|
85
|
+
* @param strategy Marker `claudeMd` field (defaults applied by the caller).
|
|
86
|
+
*/
|
|
87
|
+
export declare function planClaudeMdUpdate(current: string | null | undefined, upstream: string, strategy: ClaudeMdStrategy): ClaudeMdMergeResult;
|