forgedock 1.0.22 → 1.1.1
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/README.md +217 -218
- package/bin/cinema.mjs +320 -0
- package/bin/engine/phases.mjs +148 -0
- package/bin/engine/projector.mjs +32 -0
- package/bin/engine/reconcile.mjs +11 -0
- package/bin/engine/runlog.mjs +79 -0
- package/bin/engine/state.mjs +27 -0
- package/bin/engine-cli.mjs +50 -0
- package/bin/engine.mjs +138 -0
- package/bin/forgedock.mjs +464 -1491
- package/bin/journey.mjs +783 -0
- package/bin/runner.mjs +80 -13
- package/bin/settings-hook.mjs +112 -0
- package/bin/tui.mjs +6 -3
- package/commands/autopilot.md +4 -4
- package/commands/deploy-info.md +5 -3
- package/commands/design-bench.md +1 -0
- package/commands/design-render-critique-loop.md +1 -0
- package/commands/design.md +1 -0
- package/commands/forge-stats.md +1 -0
- package/commands/issue.md +1 -1
- package/commands/orchestrate.md +27 -9
- package/commands/quality-gate.md +80 -34
- package/commands/review-pr-agents.md +7 -7
- package/commands/review-pr.md +21 -4
- package/commands/work-on/build/implement.md +16 -4
- package/commands/work-on/build/validate.md +38 -1
- package/commands/work-on/build.md +27 -9
- package/commands/work-on/close.md +2 -2
- package/commands/work-on/review.md +24 -0
- package/commands/work-on-monolithic.md +1 -0
- package/commands/work-on.md +45 -3
- package/docs/BRAND.md +6 -2
- package/docs/CONFIG.md +39 -34
- package/docs/site/.vitepress/config.mjs +85 -0
- package/docs/site/getting-started.md +15 -8
- package/docs/site/index.md +1 -1
- package/docs/superpowers/plans/2026-07-03-cinematic-onboarding-tui.md +2339 -0
- package/docs/superpowers/plans/2026-07-03-durable-execution-engine.md +1223 -0
- package/docs/superpowers/specs/2026-07-03-cinematic-onboarding-tui-design.md +157 -0
- package/docs/superpowers/specs/2026-07-03-durable-execution-engine-design.md +194 -0
- package/package.json +10 -4
- package/scripts/verify-env-vars.sh +18 -2
- package/scripts/verify-host-headers.sh +4 -4
- package/scripts/verify-route-registration.sh +2 -2
- package/scripts/worktree-lifecycle.sh +202 -0
package/bin/runner.mjs
CHANGED
|
@@ -338,6 +338,31 @@ function resolveConfinedPath(cwd, p) {
|
|
|
338
338
|
);
|
|
339
339
|
}
|
|
340
340
|
|
|
341
|
+
// Residual TOCTOU assumption: we return the lexical `resolved` path rather
|
|
342
|
+
// than `realResolved` so that callers (readFileSync / writeFileSync) work
|
|
343
|
+
// correctly with not-yet-existing paths (where realResolved === resolved
|
|
344
|
+
// anyway, since there is nothing to dereference). The check above verified
|
|
345
|
+
// confinement on the realpath of the nearest *existing* ancestor, but a
|
|
346
|
+
// concurrent actor that mutates the filesystem between this check and the
|
|
347
|
+
// caller's open() call could in theory introduce a symlink that evades the
|
|
348
|
+
// guard — a classic time-of-check / time-of-use (TOCTOU) window.
|
|
349
|
+
//
|
|
350
|
+
// This is acceptable under the current threat model because:
|
|
351
|
+
// 1. Tool calls are executed in a strictly sequential, synchronous
|
|
352
|
+
// `for...of` loop (see the runLive tool-use loop). There is no async
|
|
353
|
+
// gap and no concurrent tool execution within the runner itself.
|
|
354
|
+
// 2. A prompt-injected model therefore cannot create this race — it would
|
|
355
|
+
// need to schedule two tool calls simultaneously, which the loop
|
|
356
|
+
// prevents.
|
|
357
|
+
// 3. Only an independent, external OS process running concurrently could
|
|
358
|
+
// exploit the window, which is outside the stated single-shot
|
|
359
|
+
// prompt-injection threat model.
|
|
360
|
+
//
|
|
361
|
+
// If the runner is ever refactored to execute tool calls concurrently or
|
|
362
|
+
// asynchronously, this assumption MUST be revisited. Mitigations to
|
|
363
|
+
// consider at that point include: operating on `realResolved` directly
|
|
364
|
+
// (eliminating the lexical/realpath split), or opening the file with an
|
|
365
|
+
// O_NOFOLLOW-equivalent flag so the kernel rejects a symlink at open time.
|
|
341
366
|
return resolved;
|
|
342
367
|
}
|
|
343
368
|
|
|
@@ -539,31 +564,48 @@ export function getToolHandlers(cwd) {
|
|
|
539
564
|
// available. `shell: shell || true` preserves execSync's implicit
|
|
540
565
|
// "always run via a shell" behavior for compound commands (&&, pipes,
|
|
541
566
|
// heredocs) when resolveBashShell() returns undefined.
|
|
567
|
+
// maxBuffer defaults to 50 MB. FORGEDOCK_MAX_BUFFER_BYTES overrides it so
|
|
568
|
+
// tests can trigger ENOBUFS with a small value without generating 50 MB of
|
|
569
|
+
// output. Non-positive or non-finite values fall back to the default.
|
|
570
|
+
const rawMaxBuffer = parseInt(process.env.FORGEDOCK_MAX_BUFFER_BYTES, 10);
|
|
571
|
+
const maxBuffer =
|
|
572
|
+
Number.isFinite(rawMaxBuffer) && rawMaxBuffer > 0
|
|
573
|
+
? rawMaxBuffer
|
|
574
|
+
: 50 * 1024 * 1024;
|
|
542
575
|
const result = spawnSync(command, {
|
|
543
576
|
cwd,
|
|
544
577
|
encoding: "utf-8",
|
|
545
|
-
maxBuffer
|
|
578
|
+
maxBuffer,
|
|
546
579
|
timeout: timeoutMs,
|
|
547
580
|
env: childEnv,
|
|
548
581
|
shell: shell || true,
|
|
549
582
|
});
|
|
550
583
|
const stdout = result.stdout ? String(result.stdout) : "";
|
|
551
584
|
const stderr = result.stderr ? String(result.stderr) : "";
|
|
552
|
-
// Detect timeout
|
|
553
|
-
// result.
|
|
554
|
-
//
|
|
555
|
-
//
|
|
556
|
-
//
|
|
557
|
-
//
|
|
558
|
-
//
|
|
559
|
-
//
|
|
560
|
-
//
|
|
585
|
+
// Detect timeout. Two reliable indicators:
|
|
586
|
+
// 1. result.error.code === "ETIMEDOUT" — Node sets this on the error
|
|
587
|
+
// object specifically when spawnSync's own `timeout` option fires
|
|
588
|
+
// and it kills the child. This is the authoritative Node-initiated
|
|
589
|
+
// kill signal.
|
|
590
|
+
// 2. elapsedMs >= timeoutMs — elapsed-wall-time fallback for platforms
|
|
591
|
+
// (primarily Windows) where the ETIMEDOUT error code is unreliable.
|
|
592
|
+
// If we spent at least as long as the timeout, the timer must have
|
|
593
|
+
// fired, since a voluntarily-exiting process would have returned
|
|
594
|
+
// before then.
|
|
595
|
+
//
|
|
596
|
+
// NOTE: `result.signal === "SIGTERM"` is intentionally NOT included.
|
|
597
|
+
// spawnSync sets result.signal for ANY signal that terminated the child —
|
|
598
|
+
// including SIGTERM from external sources (supervisors, orchestrators,
|
|
599
|
+
// `timeout(1)` wrappers, or the child self-signaling) that are entirely
|
|
600
|
+
// unrelated to this runner's timeout. Including it caused any externally-
|
|
601
|
+
// sent SIGTERM to be misreported as a ForgeDock timeout (issue #1240).
|
|
602
|
+
//
|
|
603
|
+
// Gate all of this on `result.status === null` so a legitimately-
|
|
604
|
+
// completed process near the timeout boundary is never misreported.
|
|
561
605
|
const elapsedMs = Date.now() - startMs;
|
|
562
606
|
const timedOut =
|
|
563
607
|
result.status === null &&
|
|
564
|
-
(result.error?.code === "ETIMEDOUT" ||
|
|
565
|
-
result.signal === "SIGTERM" ||
|
|
566
|
-
elapsedMs >= timeoutMs);
|
|
608
|
+
(result.error?.code === "ETIMEDOUT" || elapsedMs >= timeoutMs);
|
|
567
609
|
if (timedOut) {
|
|
568
610
|
const timeoutSecs = Math.round(timeoutMs / 1000);
|
|
569
611
|
const partial = (stdout + stderr).trim();
|
|
@@ -573,6 +615,18 @@ export function getToolHandlers(cwd) {
|
|
|
573
615
|
(partial ? `\nPartial output:\n${partial}` : ""),
|
|
574
616
|
);
|
|
575
617
|
}
|
|
618
|
+
if (result.error?.code === "ENOBUFS") {
|
|
619
|
+
// The process ran but produced more output than maxBuffer allows.
|
|
620
|
+
// spawnSync populates result.stdout/stderr up to the limit before
|
|
621
|
+
// setting result.error — surface what was captured so the agent has
|
|
622
|
+
// actionable context rather than a misleading "failed to start" error.
|
|
623
|
+
const partial = (stdout + stderr).trim();
|
|
624
|
+
const bufMB = Math.round(maxBuffer / (1024 * 1024));
|
|
625
|
+
throw new Error(
|
|
626
|
+
`Command output exceeded ${bufMB}MB buffer limit (output truncated).` +
|
|
627
|
+
(partial ? `\nPartial output:\n${partial}` : ""),
|
|
628
|
+
);
|
|
629
|
+
}
|
|
576
630
|
if (result.error) {
|
|
577
631
|
// spawnSync-level failure to even launch the process (e.g. ENOENT
|
|
578
632
|
// for a missing shell binary) — distinct from a nonzero exit status
|
|
@@ -701,6 +755,19 @@ export async function runCommand(opts = {}) {
|
|
|
701
755
|
}
|
|
702
756
|
|
|
703
757
|
const client = new Anthropic({ apiKey });
|
|
758
|
+
// NOTE on /proc/$PPID/environ (Linux): deleting from process.env calls
|
|
759
|
+
// unsetenv() at the C level, which does NOT update the kernel's
|
|
760
|
+
// /proc/<pid>/environ snapshot (that is fixed at execve() time). A
|
|
761
|
+
// run_bash payload can still read /proc/$PPID/environ and recover the
|
|
762
|
+
// key if it was set in the shell environment before the node process
|
|
763
|
+
// started. The only effective JavaScript-layer mitigation is the
|
|
764
|
+
// child-env scrub in run_bash: `childEnv = {...process.env}; delete
|
|
765
|
+
// childEnv.ANTHROPIC_API_KEY` — this prevents the child from inheriting
|
|
766
|
+
// the key in its own process.env, which is sufficient for
|
|
767
|
+
// `process.env.ANTHROPIC_API_KEY` access. The /proc/$PPID/environ vector
|
|
768
|
+
// is a known Linux limitation at the OS level; closing it would require
|
|
769
|
+
// a native module (e.g. prctl PR_SET_DUMPABLE) or process isolation.
|
|
770
|
+
// See: issue #1370 for tracking this known limitation.
|
|
704
771
|
const handlers = getToolHandlers(cwd);
|
|
705
772
|
const messages = [{ role: "user", content: userMessage }];
|
|
706
773
|
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bin/settings-hook.mjs — Idempotent SessionStart hook registration in
|
|
3
|
+
* ~/.claude/settings.json.
|
|
4
|
+
*
|
|
5
|
+
* Contract (spec: "Hook wiring"):
|
|
6
|
+
* - Read-modify-write; unrelated keys and hooks preserved verbatim
|
|
7
|
+
* - Our entry identified by HOOK_MARKER in its command string
|
|
8
|
+
* - Malformed JSON → skip and report, NEVER overwrite the file
|
|
9
|
+
* - Absolute hook script path baked in at install time
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { existsSync, readFileSync, writeFileSync, renameSync, mkdirSync } from "fs";
|
|
13
|
+
import { dirname } from "path";
|
|
14
|
+
|
|
15
|
+
/** Substring identifying ForgeDock's own hook entry. */
|
|
16
|
+
export const HOOK_MARKER = "session-start.mjs";
|
|
17
|
+
|
|
18
|
+
function isOurs(entry) {
|
|
19
|
+
return JSON.stringify(entry).includes(HOOK_MARKER);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function readSettings(settingsPath) {
|
|
23
|
+
if (!existsSync(settingsPath)) return { settings: {}, fresh: true };
|
|
24
|
+
try {
|
|
25
|
+
const parsed = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
26
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
27
|
+
return { settings: parsed, fresh: false };
|
|
28
|
+
} catch {
|
|
29
|
+
return null; // malformed — caller must not write
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function writeSettings(settingsPath, settings) {
|
|
34
|
+
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
35
|
+
const tmp = settingsPath + ".tmp";
|
|
36
|
+
writeFileSync(tmp, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
37
|
+
renameSync(tmp, settingsPath);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Merge ForgeDock's SessionStart hook into settings.json.
|
|
42
|
+
* @param {string} settingsPath - Absolute path to ~/.claude/settings.json
|
|
43
|
+
* @param {string} hookScriptPath - Absolute path to bin/hooks/session-start.mjs
|
|
44
|
+
* @returns {{ status: 'installed'|'already'|'skipped-malformed' }}
|
|
45
|
+
*/
|
|
46
|
+
export function installSessionStartHook(settingsPath, hookScriptPath) {
|
|
47
|
+
const read = readSettings(settingsPath);
|
|
48
|
+
if (read === null) return { status: "skipped-malformed" };
|
|
49
|
+
const { settings } = read;
|
|
50
|
+
|
|
51
|
+
// Validate hooks shape: must be a plain object (not array, string, etc.)
|
|
52
|
+
if (settings.hooks !== undefined && settings.hooks !== null) {
|
|
53
|
+
if (Array.isArray(settings.hooks) || typeof settings.hooks !== "object") {
|
|
54
|
+
return { status: "skipped-malformed" };
|
|
55
|
+
}
|
|
56
|
+
// Validate SessionStart if present: must be an array
|
|
57
|
+
if (settings.hooks.SessionStart !== undefined && !Array.isArray(settings.hooks.SessionStart)) {
|
|
58
|
+
return { status: "skipped-malformed" };
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
// hooks missing or null → initialize to fresh object
|
|
62
|
+
settings.hooks = {};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const list = Array.isArray(settings.hooks.SessionStart) ? settings.hooks.SessionStart : [];
|
|
66
|
+
if (list.some(isOurs)) return { status: "already" };
|
|
67
|
+
|
|
68
|
+
list.push({
|
|
69
|
+
hooks: [
|
|
70
|
+
{
|
|
71
|
+
type: "command",
|
|
72
|
+
command: `node "${hookScriptPath}"`,
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
});
|
|
76
|
+
settings.hooks.SessionStart = list;
|
|
77
|
+
writeSettings(settingsPath, settings);
|
|
78
|
+
return { status: "installed" };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Remove ForgeDock's SessionStart hook entry; leaves everything else alone.
|
|
83
|
+
* @returns {{ status: 'removed'|'absent'|'skipped-malformed' }}
|
|
84
|
+
*/
|
|
85
|
+
export function removeSessionStartHook(settingsPath) {
|
|
86
|
+
const read = readSettings(settingsPath);
|
|
87
|
+
if (read === null) return { status: "skipped-malformed" };
|
|
88
|
+
const { settings, fresh } = read;
|
|
89
|
+
if (fresh) return { status: "absent" };
|
|
90
|
+
|
|
91
|
+
// Validate hooks shape: must be a plain object (not array, string, etc.)
|
|
92
|
+
if (settings.hooks !== undefined && settings.hooks !== null) {
|
|
93
|
+
if (Array.isArray(settings.hooks) || typeof settings.hooks !== "object") {
|
|
94
|
+
return { status: "skipped-malformed" };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Validate SessionStart shape if present: must be an array
|
|
99
|
+
if (settings.hooks && settings.hooks.SessionStart !== undefined && !Array.isArray(settings.hooks.SessionStart)) {
|
|
100
|
+
return { status: "skipped-malformed" };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const list = settings.hooks && Array.isArray(settings.hooks.SessionStart)
|
|
104
|
+
? settings.hooks.SessionStart
|
|
105
|
+
: [];
|
|
106
|
+
const kept = list.filter((e) => !isOurs(e));
|
|
107
|
+
if (kept.length === list.length) return { status: "absent" };
|
|
108
|
+
|
|
109
|
+
settings.hooks.SessionStart = kept;
|
|
110
|
+
writeSettings(settingsPath, settings);
|
|
111
|
+
return { status: "removed" };
|
|
112
|
+
}
|
package/bin/tui.mjs
CHANGED
|
@@ -837,7 +837,7 @@ const REVIEW_FIELDS = [
|
|
|
837
837
|
*/
|
|
838
838
|
export async function annotatedReviewScreen(
|
|
839
839
|
draft,
|
|
840
|
-
{ hasExistingConfig = false, existingContent = "", showSources = false } = {},
|
|
840
|
+
{ hasExistingConfig = false, existingContent = "", showSources = false, extraFields = {} } = {},
|
|
841
841
|
) {
|
|
842
842
|
// Helper — pull a field from the draft by path, or return a low-confidence placeholder.
|
|
843
843
|
function getField(draftPath) {
|
|
@@ -862,7 +862,10 @@ export async function annotatedReviewScreen(
|
|
|
862
862
|
const whys = {};
|
|
863
863
|
|
|
864
864
|
for (const fd of REVIEW_FIELDS) {
|
|
865
|
-
const field =
|
|
865
|
+
const field =
|
|
866
|
+
fd.draftPath === null && extraFields[fd.key]
|
|
867
|
+
? extraFields[fd.key]
|
|
868
|
+
: getField(fd.draftPath);
|
|
866
869
|
values[fd.key] = field.value;
|
|
867
870
|
confidences[fd.key] = field.confidence;
|
|
868
871
|
sources[fd.key] = field.source;
|
|
@@ -987,7 +990,7 @@ export async function annotatedReviewScreen(
|
|
|
987
990
|
}
|
|
988
991
|
|
|
989
992
|
process.stdout.write(
|
|
990
|
-
` ${dim("Press")} ${bold("Enter")} ${dim("to
|
|
993
|
+
` ${dim("Press")} ${bold("Enter")} ${dim("to forge, or a number to edit a field")}\n\n`,
|
|
991
994
|
);
|
|
992
995
|
}
|
|
993
996
|
|
package/commands/autopilot.md
CHANGED
|
@@ -10,7 +10,7 @@ argument-hint: [--fix | --recon-only | --fix --limit 5 | --dry-run]
|
|
|
10
10
|
**Config variables used by this command** (set in `forge.yaml`):
|
|
11
11
|
- `{CREDENTIALS_FILE}` ← `paths.credentials.file` (optional) — path to credentials YAML for analytics APIs
|
|
12
12
|
- `{SERVER_SSH}` ← `services.server_ssh` (optional) — SSH target for production server health checks (e.g., `ubuntu@1.2.3.4`)
|
|
13
|
-
- `{
|
|
13
|
+
- `{OPS_INBOX_PATH}` ← `services.ops_inbox_path` (optional) — path on production server to a directory of open work-item files (e.g. `/srv/ops/inbox`)
|
|
14
14
|
- `{BILLING_ENABLED}` ← `billing.enabled` (optional, default `false`) — set to `true` to enable Stripe data collection in the Analytics Snapshot agent
|
|
15
15
|
|
|
16
16
|
Resolve `{BILLING_ENABLED}` from `forge.yaml` before executing any phase:
|
|
@@ -112,7 +112,7 @@ Spawn these as **background agents** (all `run_in_background=true`, all `model="
|
|
|
112
112
|
**Agent: Production Health**
|
|
113
113
|
```
|
|
114
114
|
Check production system health:
|
|
115
|
-
1. SSH to check for open
|
|
115
|
+
1. SSH to check for open ops-inbox items: if {SERVER_SSH} is configured, run: ssh {SERVER_SSH} "ls {OPS_INBOX_PATH}/*-open-* 2>/dev/null" — skip this step if SERVER_SSH or OPS_INBOX_PATH is not set in forge.yaml
|
|
116
116
|
2. If any exist, cat each one and summarize
|
|
117
117
|
3. Check container health via MCP: get_production_status, run_production_health_check
|
|
118
118
|
4. Check recent error logs: get_production_logs for api and worker (last 100 lines), grep for ERROR/CRITICAL
|
|
@@ -197,7 +197,7 @@ For each finding from RECON_DATA, assign:
|
|
|
197
197
|
|
|
198
198
|
| Signal | Priority | Type |
|
|
199
199
|
|--------|----------|------|
|
|
200
|
-
|
|
|
200
|
+
| Open ops-inbox item | P1 | bug |
|
|
201
201
|
| Container unhealthy | P0 | bug |
|
|
202
202
|
| Error spike (>5x normal) | P1 | bug |
|
|
203
203
|
| Tier degradation (>20% drop) | P2 | bug |
|
|
@@ -286,7 +286,7 @@ Print a structured report:
|
|
|
286
286
|
|
|
287
287
|
### Production Health
|
|
288
288
|
- Containers: {all healthy / N unhealthy}
|
|
289
|
-
- Open
|
|
289
|
+
- Open ops-inbox items: {count}
|
|
290
290
|
- Error patterns: {summary or "none"}
|
|
291
291
|
|
|
292
292
|
### Scraping Performance
|
package/commands/deploy-info.md
CHANGED
|
@@ -156,7 +156,7 @@ done
|
|
|
156
156
|
# Full file diff summary
|
|
157
157
|
git diff --stat origin/$TARGET..origin/$SOURCE
|
|
158
158
|
|
|
159
|
-
# Read layout paths from forge.yaml, fall back to
|
|
159
|
+
# Read layout paths from forge.yaml, fall back to forge.yaml defaults
|
|
160
160
|
LAYOUT_API=$(yq '.review.layout.api // "services/api"' forge.yaml 2>/dev/null || echo "services/api")
|
|
161
161
|
LAYOUT_WORKER=$(yq '.review.layout.worker // "services/worker"' forge.yaml 2>/dev/null || echo "services/worker")
|
|
162
162
|
LAYOUT_PAGES=$(yq '.review.layout.pages // "web/src/app"' forge.yaml 2>/dev/null || echo "web/src/app")
|
|
@@ -231,8 +231,10 @@ if [ -n "$NEW_ENV" ]; then
|
|
|
231
231
|
# If your project uses a different backend, skip this block and verify manually.
|
|
232
232
|
if [ "{DEPLOY_SECRETS_BACKEND}" = "sops" ]; then
|
|
233
233
|
for var in $(echo "$NEW_ENV" | grep -oE '^\+[A-Z_]+' | sed 's/^\+//'); do
|
|
234
|
-
if
|
|
235
|
-
echo " ❌ $var NOT in decrypt-secrets.sh ENV_MAPPING"
|
|
234
|
+
if [ -f "{REPO_PATH}/scripts/decrypt-secrets.sh" ]; then
|
|
235
|
+
grep -q "$var" "{REPO_PATH}/scripts/decrypt-secrets.sh" 2>/dev/null || echo " ❌ $var NOT in decrypt-secrets.sh ENV_MAPPING"
|
|
236
|
+
else
|
|
237
|
+
echo " ℹ️ scripts/decrypt-secrets.sh not found — skipping ENV_MAPPING cross-check for $var. Verify manually in your SOPS chain."
|
|
236
238
|
fi
|
|
237
239
|
done
|
|
238
240
|
else
|
package/commands/design-bench.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Run the ABC benchmark — reference URL → design-blind brief → render arms A/B/C → blind judge → scorecard with win-rates over n runs
|
|
3
3
|
argument-hint: [<brief-name>|all] [--n <runs>] [--corpus-version <ver>]
|
|
4
|
+
install: internal
|
|
4
5
|
---
|
|
5
6
|
<!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
|
|
6
7
|
<!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Render a design from its FORGE:DESIGN_SPEC, critique the rendered pixels against the perceptual negatives, and iterate until it passes or the budget is spent — the missing reward signal.
|
|
3
3
|
argument-hint: [<issue-number>|<spec-path>] [--max-iters <n>] [--viewports desktop,mobile]
|
|
4
|
+
install: internal
|
|
4
5
|
---
|
|
5
6
|
<!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
|
|
6
7
|
<!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
|
package/commands/design.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Route a design GitHub issue through the UI Taste Harness — investigate → architect → implement → critique loop → user-feedback loop → close — accumulating FORGE:DESIGN_* annotations and driving the design:* label state machine to design:shipped.
|
|
3
3
|
argument-hint: <issue-number> | <brief-name>
|
|
4
|
+
install: internal
|
|
4
5
|
---
|
|
5
6
|
<!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
|
|
6
7
|
<!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
|
package/commands/forge-stats.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Track command sizes, detect bloat, compare against baselines and thresholds
|
|
3
3
|
argument-hint: [diff | baseline | full]
|
|
4
|
+
install: internal
|
|
4
5
|
---
|
|
5
6
|
<!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
|
|
6
7
|
<!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
|
package/commands/issue.md
CHANGED
|
@@ -495,7 +495,7 @@ Create all {N} issues? (yes / adjust #N / cancel)
|
|
|
495
495
|
|
|
496
496
|
| Bad Issue | Why It Fails | Good Version |
|
|
497
497
|
|-----------|-------------|-------------|
|
|
498
|
-
| "Fix docker mount" | Which compose file? Which mount? | "fix:
|
|
498
|
+
| "Fix docker mount" | Which compose file? Which mount? | "fix: `logs` volume in docker-compose.yml missing `rw` flag — container writes fail at runtime" |
|
|
499
499
|
| "Update billing" | What about billing? What's broken? | "fix: payment_processor.charge() raises ZeroDivisionError when amount == 0" |
|
|
500
500
|
| "Investigate performance" | Investigate what? Where? | "investigate: API p95 latency doubled since last deploy — trace hot paths in job processing routers" |
|
|
501
501
|
| No affected files listed | Investigator guesses wrong files | List every file with what needs to change |
|
package/commands/orchestrate.md
CHANGED
|
@@ -519,9 +519,11 @@ When two issues modify different files that **import from the same utility/init
|
|
|
519
519
|
| Pattern | Inference | Action |
|
|
520
520
|
|---------|-----------|--------|
|
|
521
521
|
| Issue A modifies `routers/billing.py`, Issue B modifies `routers/auth.py` | Both likely import from `routers/__init__.py` or `dependencies.py` | Flag as possible conflict if same service |
|
|
522
|
-
| Issue A modifies `models/user.py`, Issue B modifies `models/subscription.py` | Both likely modify `models/__init__.py` (
|
|
522
|
+
| Issue A modifies `models/user.py`, Issue B modifies `models/subscription.py` *(Python/SQLAlchemy example)* | Both likely modify `models/__init__.py` (model registry) | Flag as probable conflict → serialize |
|
|
523
|
+
| Issue A modifies `routes/users.js`, Issue B modifies `routes/billing.js` *(Node.js example)* | Both likely import from `routes/index.js` barrel | Flag as possible conflict if same service |
|
|
524
|
+
| Issue A modifies `internal/user/handler.go`, Issue B modifies `internal/billing/handler.go` *(Go example)* | Both likely register in `internal/router/router.go` | Flag as possible conflict if same service |
|
|
523
525
|
| Issue A modifies `web/src/components/X.tsx`, Issue B modifies `web/src/components/Y.tsx` | May share `index.ts` barrel export | Flag only if same parent directory |
|
|
524
|
-
| Issue A modifies `
|
|
526
|
+
| Issue A modifies the app entrypoint (e.g. `main.py`, `main.go`, `server.js`, `app.ts`) | Entrypoint is a high-fan-in file (router registration, middleware) — set the path via `forge.yaml → review.layout.api_main` | Serialize with ANY other same-service issue |
|
|
525
527
|
| Issue A modifies `docker-compose.yml` or `docker-compose.prod.yml` | Global config — any concurrent modification conflicts | Serialize with ALL other issues that touch infra |
|
|
526
528
|
|
|
527
529
|
**Apply inferences:**
|
|
@@ -529,14 +531,14 @@ When two issues modify different files that **import from the same utility/init
|
|
|
529
531
|
# High-fan-in files — if ANY issue touches these, serialize it with all same-service issues.
|
|
530
532
|
# Read layout paths from forge.yaml review.layout; fall back to sensible generic defaults.
|
|
531
533
|
# Example (pseudo-code — adapt to your forge.yaml parsing method):
|
|
532
|
-
# API_MAIN = forge_yaml.review.layout.api_main ?? "
|
|
533
|
-
# WORKER_MAIN = forge_yaml.review.layout.worker_main ?? "
|
|
534
|
-
# PAGES_ROOT = forge_yaml.review.layout.pages ?? "web/src/app"
|
|
534
|
+
# API_MAIN = forge_yaml.review.layout.api_main ?? "src/main.py" # set in forge.yaml; no stack-specific default
|
|
535
|
+
# WORKER_MAIN = forge_yaml.review.layout.worker_main ?? "src/worker.py" # set in forge.yaml; no stack-specific default
|
|
536
|
+
# PAGES_ROOT = forge_yaml.review.layout.pages ?? "web/src/app" # Next.js default; override in forge.yaml for other frameworks
|
|
535
537
|
|
|
536
538
|
HIGH_FAN_IN = [
|
|
537
|
-
API_MAIN, #
|
|
538
|
-
WORKER_MAIN, #
|
|
539
|
-
PAGES_ROOT + "/layout.tsx", #
|
|
539
|
+
API_MAIN, # app entrypoint — router/middleware registration (set forge.yaml review.layout.api_main)
|
|
540
|
+
WORKER_MAIN, # worker entrypoint (set forge.yaml review.layout.worker_main)
|
|
541
|
+
PAGES_ROOT + "/layout.tsx", # root layout for all pages (Next.js; adapt for your framework)
|
|
540
542
|
"docker-compose.yml",
|
|
541
543
|
"docker-compose.prod.yml",
|
|
542
544
|
".env.example"
|
|
@@ -570,13 +572,17 @@ Layers 1-4 infer conflict risk from structure — path overlap, directory nestin
|
|
|
570
572
|
# argument below — a plain string here would be word-split and glob-expanded
|
|
571
573
|
# by the shell when handed to `git log --`, silently mangling or dropping any
|
|
572
574
|
# path containing a space or glob metacharacter.
|
|
573
|
-
mapfile -t ALL_AFFECTED_FILES < <(printf '%s\n' "${LAYER1_FILES[@]}" | sort -u)
|
|
575
|
+
mapfile -t ALL_AFFECTED_FILES < <(printf '%s\n' "${LAYER1_FILES[@]}" | sort -u | grep -v '^$')
|
|
574
576
|
|
|
575
577
|
# Guard: an empty array expands to nothing after `--`, which git interprets as
|
|
576
578
|
# "no pathspec restriction" (i.e. the whole repo) rather than "match nothing".
|
|
577
579
|
# Skip the query entirely in that case instead of letting it silently widen to
|
|
578
580
|
# a full-repo scan — this can happen when upstream file-extraction (Layer 1)
|
|
579
581
|
# yields zero paths for every issue in the batch.
|
|
582
|
+
# NOTE: `grep -v '^$'` above is required — without it, `printf '%s\n'` on an
|
|
583
|
+
# empty array emits one blank line (printf runs its format string once even
|
|
584
|
+
# with zero variadic args), which `mapfile` captures as a 1-element array of
|
|
585
|
+
# [""], making this guard evaluate to false and causing `git log -- ""` to run.
|
|
580
586
|
if [ "${#ALL_AFFECTED_FILES[@]}" -eq 0 ]; then
|
|
581
587
|
echo "Layer 5: no affected files extracted by Layer 1 for this batch — skipping co-change query, falling back to Layers 1-4."
|
|
582
588
|
else
|
|
@@ -834,6 +840,18 @@ Proceed? (yes / adjust / pick specific issues)
|
|
|
834
840
|
|
|
835
841
|
---
|
|
836
842
|
|
|
843
|
+
## Engine mode (headless)
|
|
844
|
+
|
|
845
|
+
When running headless/CI, dispatch each issue via the durable execution engine instead of spawning prose agents:
|
|
846
|
+
|
|
847
|
+
```bash
|
|
848
|
+
forgedock run-issue <issue> --lane <staging|milestone/slug>
|
|
849
|
+
```
|
|
850
|
+
|
|
851
|
+
The engine drives every phase transition deterministically, mirrors state to the `FORGE:STATE` block on the issue, and holds a lease. To recover stalls, scan in-flight issues' `FORGE:STATE`; any issue with an expired lease and a non-terminal state is re-dispatched with the same `forgedock run-issue <issue>` command — it resumes from the last committed phase (idempotent). This replaces the label-heuristic "already in progress" check and the resume-with-nagging loop.
|
|
852
|
+
|
|
853
|
+
---
|
|
854
|
+
|
|
837
855
|
## Phase 4: Streaming DAG Execution
|
|
838
856
|
|
|
839
857
|
### Step 4A-pre: Staging baseline tracking (MANDATORY — continuous)
|