forgedock 1.2.0 → 1.3.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/README.md +8 -2
- package/bin/engine/phases.mjs +178 -28
- package/bin/engine/state.mjs +39 -7
- package/bin/engine-cli.mjs +63 -2
- package/bin/engine.mjs +25 -4
- package/bin/forgedock.mjs +235 -4
- package/bin/hooks/pre-tool-use.mjs +26 -12
- package/bin/journey.mjs +103 -11
- package/commands/forge-stats.md +5 -4
- package/commands/issue.md +5 -1
- package/commands/orchestrate/phase-4-execution.md +2 -0
- package/commands/qa-sweep.md +17 -0
- package/commands/replay.md +16 -2
- package/commands/review-pr-agents/spec-cli.md +179 -0
- package/commands/review-pr.md +49 -0
- package/commands/upgrade-deps.md +518 -0
- package/commands/work-on/build/validate.md +61 -1
- package/commands/work-on/build.md +9 -1
- package/commands/work-on/decompose.md +8 -1
- package/docs/AIDER.md +264 -0
- package/docs/AUTOPILOT-CRON.md +130 -0
- package/docs/CONFIG.md +36 -0
- package/docs/CURSOR.md +135 -0
- package/docs/OPENCODE.md +312 -0
- package/docs/demo.gif +0 -0
- package/docs/eval/README.md +4 -4
- package/docs/launch/show-hn.md +3 -3
- package/docs/site/architecture.md +180 -0
- package/docs/site/command-reference.md +17 -0
- package/docs/site/configuration.md +243 -0
- package/docs/site/for-companies.md +4 -4
- package/docs/site/forge-annotation-protocol.md +1 -1
- package/docs/site/github-is-the-memory.md +2 -2
- package/docs/site/how-it-works.md +1 -1
- package/docs/site/public/llms-full.txt +380 -0
- package/docs/site/public/llms.txt +35 -0
- package/docs/spec-graph-schema.md +3 -3
- package/package.json +2 -2
- package/packages/protocol/README.md +75 -0
- package/packages/protocol/src/card.js +114 -0
- package/packages/protocol/src/cli.js +4 -77
- package/packages/protocol/src/emit.js +14 -3
- package/packages/protocol/src/index.js +2 -1
- package/packages/protocol/src/parse.js +17 -8
- package/packages/protocol/src/types.js +1 -1
- package/packages/protocol/src/validate.js +11 -0
- package/scripts/check-command-side-effects.sh +261 -0
- package/scripts/check-spec-markers.sh +273 -0
- package/scripts/classify-lane.sh +29 -7
- package/scripts/forge-run.sh +322 -0
- package/templates/cross-repo-trigger.yml +282 -0
- package/templates/cursor/forge.mdc +148 -0
- package/templates/github-actions/forgedock-autopilot.yml +113 -0
package/bin/forgedock.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
4
|
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
|
-
import { dirname, join, relative, resolve } from "path";
|
|
6
|
+
import { dirname, join, relative, resolve, sep } from "path";
|
|
7
7
|
import { mkdir, lstat, readlink, readdir, unlink, readFile, writeFile } from "fs/promises";
|
|
8
8
|
import {
|
|
9
9
|
existsSync,
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
renameSync,
|
|
14
14
|
unlinkSync,
|
|
15
15
|
} from "fs";
|
|
16
|
-
import { execSync, execFileSync } from "child_process";
|
|
16
|
+
import { execSync, execFileSync, spawnSync } from "child_process";
|
|
17
17
|
import { homedir } from "os";
|
|
18
18
|
import https from "https";
|
|
19
19
|
import {
|
|
@@ -1475,6 +1475,216 @@ async function printVersionAvailableChangelog(localVersion, latestVersion) {
|
|
|
1475
1475
|
}
|
|
1476
1476
|
}
|
|
1477
1477
|
|
|
1478
|
+
/**
|
|
1479
|
+
* Detect whether FORGE_HOME resolves inside npm's global install tree, i.e.
|
|
1480
|
+
* this process is running as a globally-installed `forgedock` package (not
|
|
1481
|
+
* an ephemeral npx/dlx cache extraction). Used by update()'s npm-mode branch
|
|
1482
|
+
* (forge#2133) to decide whether a newer published version can be installed
|
|
1483
|
+
* in place with `npm install -g forgedock@latest`, versus the npx-cache case
|
|
1484
|
+
* where there is nothing durable to reinstall over.
|
|
1485
|
+
*
|
|
1486
|
+
* Resolution: `npm root -g` reports the global node_modules directory; if
|
|
1487
|
+
* FORGE_HOME sits under `{globalRoot}/forgedock`, this is a global install.
|
|
1488
|
+
* Fails closed (returns false) on any error — the pre-existing advisory-only
|
|
1489
|
+
* behavior is always a safe fallback when detection is inconclusive.
|
|
1490
|
+
*
|
|
1491
|
+
* @returns {boolean}
|
|
1492
|
+
*/
|
|
1493
|
+
function isGlobalNpmInstall() {
|
|
1494
|
+
try {
|
|
1495
|
+
const globalRoot = execSync("npm root -g", {
|
|
1496
|
+
encoding: "utf-8",
|
|
1497
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1498
|
+
timeout: 5000,
|
|
1499
|
+
}).trim();
|
|
1500
|
+
if (!globalRoot) return false;
|
|
1501
|
+
const globalPkgDir = resolve(join(globalRoot, "forgedock"));
|
|
1502
|
+
const home = resolve(FORGE_HOME);
|
|
1503
|
+
return home === globalPkgDir || home.startsWith(globalPkgDir + sep);
|
|
1504
|
+
} catch {
|
|
1505
|
+
return false;
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
/**
|
|
1510
|
+
* Maximum number of additional self-update re-exec attempts allowed after the
|
|
1511
|
+
* first (i.e. up to MAX_SELF_UPDATE_ATTEMPTS + 1 total `npm install -g`
|
|
1512
|
+
* attempts across the parent process and its re-exec'd children). Guards
|
|
1513
|
+
* against unbounded recursion when `npm install -g` reports success but the
|
|
1514
|
+
* resolved version never actually advances (stale registry mirror/proxy,
|
|
1515
|
+
* cache issue, mismatched FORGE_HOME resolution) — see forge#2158.
|
|
1516
|
+
*/
|
|
1517
|
+
const MAX_SELF_UPDATE_ATTEMPTS = 1;
|
|
1518
|
+
|
|
1519
|
+
/**
|
|
1520
|
+
* Strict semver-shape check for a version string before it is interpolated
|
|
1521
|
+
* into a shell-invoked command (see forge#2180). Intentionally narrow —
|
|
1522
|
+
* digits-dot-digits-dot-digits with an optional `-prerelease`/`+build`
|
|
1523
|
+
* suffix restricted to `[0-9A-Za-z.-]` — so it accepts every real value
|
|
1524
|
+
* `fetchLatestVersion()` can return from the npm registry's `version` field,
|
|
1525
|
+
* while rejecting anything containing shell metacharacters
|
|
1526
|
+
* (`; | & $ \` ( ) < > " ' \n` etc.) that `shell: true` would otherwise
|
|
1527
|
+
* hand straight to cmd.exe/`/bin/sh`.
|
|
1528
|
+
*
|
|
1529
|
+
* The regex above is fully anchored (`^...$`) but its quantifiers (`\d+`,
|
|
1530
|
+
* `[0-9A-Za-z.-]+`) are unbounded, so shape validity alone does not bound
|
|
1531
|
+
* string *length* — a pathologically long value could still match. This
|
|
1532
|
+
* is defense-in-depth only (see forge#2195): the character-class
|
|
1533
|
+
* restriction already rules out shell metacharacters at any length, and
|
|
1534
|
+
* `fetchLatestVersion()`'s 65536-byte HTTP response cap already bounds
|
|
1535
|
+
* `version` in practice. The explicit length cap below rejects oversized
|
|
1536
|
+
* input before the regex ever runs, closing the gap outright rather than
|
|
1537
|
+
* relying on an upstream cap this function has no visibility into.
|
|
1538
|
+
*
|
|
1539
|
+
* @param {string} version
|
|
1540
|
+
* @returns {boolean}
|
|
1541
|
+
*/
|
|
1542
|
+
function isValidSemverShape(version) {
|
|
1543
|
+
const MAX_VERSION_LENGTH = 100;
|
|
1544
|
+
return (
|
|
1545
|
+
typeof version === "string" &&
|
|
1546
|
+
version.length <= MAX_VERSION_LENGTH &&
|
|
1547
|
+
/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(version)
|
|
1548
|
+
);
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
/**
|
|
1552
|
+
* Install the newer published version globally via `npm install -g
|
|
1553
|
+
* forgedock@{version}`, then re-exec this same file path (which npm has just
|
|
1554
|
+
* overwritten with the new package's contents) so the rest of update()'s
|
|
1555
|
+
* persist/relink phase runs against the *new* payload instead of the stale
|
|
1556
|
+
* in-memory one (forge#2133 — previously the advisory-only branch would
|
|
1557
|
+
* persist/relink from whatever was already resolved, silently downgrading
|
|
1558
|
+
* a newer persisted ~/.forge/ or leaving the global install stuck on the
|
|
1559
|
+
* old version indefinitely).
|
|
1560
|
+
*
|
|
1561
|
+
* Depth guard (forge#2158): the re-exec'd child process could, in principle,
|
|
1562
|
+
* observe the same "newer version available" condition again (e.g. a stale
|
|
1563
|
+
* registry mirror/proxy reports success without the resolved version ever
|
|
1564
|
+
* advancing) and recurse indefinitely, each cycle blocking up to ~125s. The
|
|
1565
|
+
* attempt count is propagated across the re-exec via the
|
|
1566
|
+
* `FORGEDOCK_SELF_UPDATE_ATTEMPT` environment variable (parent process env is
|
|
1567
|
+
* never mutated — the counter is only passed to the spawned child's own
|
|
1568
|
+
* environment). Once `MAX_SELF_UPDATE_ATTEMPTS` is reached, this function
|
|
1569
|
+
* fails loudly with an actionable message and returns `false` instead of
|
|
1570
|
+
* attempting another install/re-exec cycle.
|
|
1571
|
+
*
|
|
1572
|
+
* Fail-open: any error during install or re-exec falls back to the
|
|
1573
|
+
* pre-existing advisory message — the caller must treat a `false` return as
|
|
1574
|
+
* "could not self-update, printed manual instructions instead."
|
|
1575
|
+
*
|
|
1576
|
+
* Windows note (forge#2180): `npm` is a `.cmd` batch launcher on Windows, and
|
|
1577
|
+
* Node's `execFileSync`/`spawnSync` cannot invoke `.cmd`/`.bat` files without
|
|
1578
|
+
* `shell: true` (nodejs/node#3675) — confirmed empirically on a real Windows
|
|
1579
|
+
* 11 machine: `execFileSync("npm", [...])` ENOENTs, and even the exact
|
|
1580
|
+
* `npm.cmd` filename EINVALs, without a shell. `shell: true` is required
|
|
1581
|
+
* here so the install actually runs on Windows. Because that widens the
|
|
1582
|
+
* command line to shell interpretation, `version` — sourced from an
|
|
1583
|
+
* unauthenticated `https://registry.npmjs.org` response in
|
|
1584
|
+
* `fetchLatestVersion()` — is validated against a strict semver shape
|
|
1585
|
+
* (`isValidSemverShape`) before it is ever interpolated into the command,
|
|
1586
|
+
* closing the injection surface `shell: true` would otherwise open (see the
|
|
1587
|
+
* repo-wide execSync→execFileSync shell-injection hardening precedent in
|
|
1588
|
+
* forge#1703/forge#413).
|
|
1589
|
+
*
|
|
1590
|
+
* @param {string} version - target version to install (e.g. "1.2.0")
|
|
1591
|
+
* @returns {boolean} true if re-exec was launched (process will exit before
|
|
1592
|
+
* returning in the success path); false if self-update failed, was capped
|
|
1593
|
+
* by the depth guard, and the caller should fall back to advisory-only
|
|
1594
|
+
* behavior.
|
|
1595
|
+
*/
|
|
1596
|
+
function selfUpdateGlobalInstall(version) {
|
|
1597
|
+
// Math.max(0, ...) clamps out negative values: `Number.parseInt(x, 10) || 0`
|
|
1598
|
+
// only substitutes 0 for NaN/0/"" — a negative numeric string (e.g. a
|
|
1599
|
+
// corrupted or manually-set FORGEDOCK_SELF_UPDATE_ATTEMPT="-1") is truthy
|
|
1600
|
+
// and passes through unclamped, which would let a negative attempt count
|
|
1601
|
+
// stay perpetually below MAX_SELF_UPDATE_ATTEMPTS. Defense-in-depth only —
|
|
1602
|
+
// no known path sets this env var to a negative value today. (forge#2168)
|
|
1603
|
+
const attempt = Math.max(
|
|
1604
|
+
0,
|
|
1605
|
+
Number.parseInt(process.env.FORGEDOCK_SELF_UPDATE_ATTEMPT, 10) || 0,
|
|
1606
|
+
);
|
|
1607
|
+
|
|
1608
|
+
// `attempt` counts completed installs so far (0 = no installs yet). The
|
|
1609
|
+
// guard must allow MAX_SELF_UPDATE_ATTEMPTS + 1 total installs (see the
|
|
1610
|
+
// doc comment above this function) — i.e. it should only trip once
|
|
1611
|
+
// `attempt` has already reached that total, not one call early. Using
|
|
1612
|
+
// `>=` here tripped after only 1 install instead of the documented 2
|
|
1613
|
+
// (forge#2203 — the retry this guard exists to allow never actually ran).
|
|
1614
|
+
if (attempt > MAX_SELF_UPDATE_ATTEMPTS) {
|
|
1615
|
+
console.log(
|
|
1616
|
+
` ${YELLOW}Self-update did not converge after ${attempt} attempt(s) — the installed version may not be advancing (stale registry mirror/cache?).${RESET}`,
|
|
1617
|
+
);
|
|
1618
|
+
console.log(
|
|
1619
|
+
` Run ${CYAN}npm install -g forgedock@latest${RESET} manually, then ${CYAN}npx forgedock update${RESET} again.`,
|
|
1620
|
+
);
|
|
1621
|
+
return false;
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
// Validate before this value ever reaches the shell-invoked install below
|
|
1625
|
+
// (forge#2180) — fail open to the same advisory message a failed install
|
|
1626
|
+
// would print, rather than throwing or passing an unvalidated string to
|
|
1627
|
+
// `shell: true`.
|
|
1628
|
+
if (!isValidSemverShape(version)) {
|
|
1629
|
+
console.log(
|
|
1630
|
+
` ${YELLOW}Self-update aborted: received an unexpected version string ("${version}").${RESET}`,
|
|
1631
|
+
);
|
|
1632
|
+
console.log(
|
|
1633
|
+
` Run ${CYAN}npm install -g forgedock@latest${RESET} manually, then ${CYAN}npx forgedock update${RESET} again.`,
|
|
1634
|
+
);
|
|
1635
|
+
return false;
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
try {
|
|
1639
|
+
console.log(` Installing ${CYAN}forgedock@${version}${RESET} globally...`);
|
|
1640
|
+
// shell: true is required to resolve npm.cmd on Windows (see the
|
|
1641
|
+
// Windows note in this function's doc comment, forge#2180). `version`
|
|
1642
|
+
// is validated as strict semver above before this point.
|
|
1643
|
+
execFileSync("npm", ["install", "-g", `forgedock@${version}`], {
|
|
1644
|
+
stdio: "inherit",
|
|
1645
|
+
timeout: 120000,
|
|
1646
|
+
shell: true,
|
|
1647
|
+
});
|
|
1648
|
+
} catch (err) {
|
|
1649
|
+
console.log(
|
|
1650
|
+
` ${YELLOW}Global self-update failed: ${err && err.message ? err.message : String(err)}${RESET}`,
|
|
1651
|
+
);
|
|
1652
|
+
console.log(
|
|
1653
|
+
` Run ${CYAN}npm install -g forgedock@latest${RESET} manually, then ${CYAN}npx forgedock update${RESET} again.`,
|
|
1654
|
+
);
|
|
1655
|
+
return false;
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
console.log(
|
|
1659
|
+
` ${GREEN}Installed v${version}.${RESET} Re-running update to finish persist/relink...`,
|
|
1660
|
+
);
|
|
1661
|
+
try {
|
|
1662
|
+
const result = spawnSync(process.execPath, [__filename, "update"], {
|
|
1663
|
+
stdio: "inherit",
|
|
1664
|
+
env: {
|
|
1665
|
+
...process.env,
|
|
1666
|
+
FORGEDOCK_SELF_UPDATE_ATTEMPT: String(attempt + 1),
|
|
1667
|
+
},
|
|
1668
|
+
});
|
|
1669
|
+
// `result.signal` is set (non-null) when the re-exec'd child was
|
|
1670
|
+
// terminated by a signal (e.g. SIGTERM/SIGKILL) rather than exiting
|
|
1671
|
+
// cleanly — in that case `result.status` is `null`, and
|
|
1672
|
+
// `result.status ?? 0` would silently collapse a killed child into a
|
|
1673
|
+
// reported "success" (exit 0). Exit non-zero explicitly for the
|
|
1674
|
+
// signal-killed case; the non-signaled path is unchanged. (forge#2159)
|
|
1675
|
+
process.exit(result.signal ? 1 : (result.status ?? 0));
|
|
1676
|
+
// Unreachable — process.exit() above terminates the process. Present
|
|
1677
|
+
// only to satisfy linters expecting a return on every path.
|
|
1678
|
+
return true;
|
|
1679
|
+
} catch (err) {
|
|
1680
|
+
console.log(
|
|
1681
|
+
` ${YELLOW}Could not re-run update after install: ${err && err.message ? err.message : String(err)}${RESET}`,
|
|
1682
|
+
);
|
|
1683
|
+
console.log(` Run ${CYAN}npx forgedock update${RESET} again manually.`);
|
|
1684
|
+
return false;
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1478
1688
|
async function update() {
|
|
1479
1689
|
console.log("");
|
|
1480
1690
|
console.log(`${BOLD}ForgeDock${RESET} — Checking for updates`);
|
|
@@ -1569,8 +1779,25 @@ async function update() {
|
|
|
1569
1779
|
console.log(
|
|
1570
1780
|
` ${GREEN}New version available: v${latestVersion}${RESET} (you have v${localVersion}).`,
|
|
1571
1781
|
);
|
|
1572
|
-
console.log(` Run ${CYAN}npx forgedock@latest${RESET} to fetch it.`);
|
|
1573
1782
|
await printVersionAvailableChangelog(localVersion, latestVersion);
|
|
1783
|
+
|
|
1784
|
+
// forge#2133 — a global npm install can actually be upgraded in place;
|
|
1785
|
+
// do that instead of only printing an advisory (which previously left
|
|
1786
|
+
// global installs stuck on whatever version was first installed,
|
|
1787
|
+
// since every subsequent `npx forgedock update` re-resolves the same
|
|
1788
|
+
// stale global binary). selfUpdateGlobalInstall() re-execs and exits
|
|
1789
|
+
// the process on success, so anything after this block only runs when
|
|
1790
|
+
// self-update wasn't attempted or failed.
|
|
1791
|
+
if (isGlobalNpmInstall()) {
|
|
1792
|
+
selfUpdateGlobalInstall(latestVersion);
|
|
1793
|
+
// selfUpdateGlobalInstall() only returns (rather than exiting the
|
|
1794
|
+
// process) when the install or re-exec failed — it already printed
|
|
1795
|
+
// manual-recovery instructions, so fall through to the persist
|
|
1796
|
+
// refresh below using the still-stale local payload (fail-open,
|
|
1797
|
+
// matches pre-existing behavior for this failure case).
|
|
1798
|
+
} else {
|
|
1799
|
+
console.log(` Run ${CYAN}npx forgedock@latest${RESET} to fetch it.`);
|
|
1800
|
+
}
|
|
1574
1801
|
} else {
|
|
1575
1802
|
console.log(` Already up to date (v${localVersion}).`);
|
|
1576
1803
|
}
|
|
@@ -3238,7 +3465,11 @@ switch (command) {
|
|
|
3238
3465
|
case "run-issue": {
|
|
3239
3466
|
const { runFromCli } = await import("./engine-cli.mjs");
|
|
3240
3467
|
try {
|
|
3241
|
-
await runFromCli(restArgs);
|
|
3468
|
+
const result = await runFromCli(restArgs);
|
|
3469
|
+
// forge#2175: make a needs-human escalation distinguishable from a
|
|
3470
|
+
// successful run by exit code, mirroring the resume-stalled case's
|
|
3471
|
+
// existing convention below (result.failed.length > 0 -> exitCode 1).
|
|
3472
|
+
if (result && result.terminalReason === "needs-human") exitCode = 1;
|
|
3242
3473
|
} catch (err) {
|
|
3243
3474
|
process.stderr.write(`${RED}${err.message}${RESET}\n`);
|
|
3244
3475
|
exitCode = 1;
|
|
@@ -180,13 +180,23 @@ const WORKFLOW_LABEL_PREFIX = "workflow:";
|
|
|
180
180
|
// ---------------------------------------------------------------------------
|
|
181
181
|
|
|
182
182
|
/**
|
|
183
|
-
* Match a `find` search-root token that
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
183
|
+
* Match a `find` search-root token that reaches the exact same whole-drive,
|
|
184
|
+
* non-terminating scan on Git Bash as the bare filesystem root (`/`) or a
|
|
185
|
+
* bare drive mount (`/c`, `/d`, ...). This includes trailing-slash drive
|
|
186
|
+
* mounts (`/c/`) — a natural way to write the same root, since `/c` and
|
|
187
|
+
* `/c/` are the identical directory — and the dot/double-slash root
|
|
188
|
+
* variants `/.`, `/..`, `//`, which likewise resolve to (or immediately
|
|
189
|
+
* above, which Git Bash still normalizes to) the same whole-drive tree
|
|
190
|
+
* (issue #2113 — PR #2112 review finding SEC-1: `find /c/` was silently
|
|
191
|
+
* ALLOWED because the prior regex had no trailing-slash/dot alternative).
|
|
192
|
+
*
|
|
193
|
+
* Deliberately does NOT match `/c/Users/...`, `/./foo`, `/../foo`, or any
|
|
194
|
+
* other scoped absolute path — only the bare root/drive-mount token itself,
|
|
195
|
+
* optionally followed by exactly one trailing slash (drive-letter case) or
|
|
196
|
+
* standing alone (dot/double-slash case). Case-insensitive on the drive
|
|
197
|
+
* letter only (POSIX paths are otherwise case-sensitive).
|
|
188
198
|
*/
|
|
189
|
-
const FIND_ROOT_TOKEN_RE = /^\/(?:[a-zA-Z])?$/;
|
|
199
|
+
const FIND_ROOT_TOKEN_RE = /^\/(?:[a-zA-Z]\/?|\.{1,2}|\/)?$/;
|
|
190
200
|
|
|
191
201
|
/**
|
|
192
202
|
* Shell metacharacters that can glue a command name to an adjacent token
|
|
@@ -682,12 +692,16 @@ function extractLogicalTokens(command) {
|
|
|
682
692
|
|
|
683
693
|
/**
|
|
684
694
|
* Check whether a Bash command contains a `find` invocation whose search
|
|
685
|
-
* root is the filesystem root (`/`)
|
|
686
|
-
* `/d`, ...)
|
|
687
|
-
* root
|
|
688
|
-
*
|
|
689
|
-
*
|
|
690
|
-
*
|
|
695
|
+
* root is the filesystem root (`/`), a bare Git-Bash drive mount (`/c`,
|
|
696
|
+
* `/d`, ...), a trailing-slash drive mount (`/c/`), or a dot/double-slash
|
|
697
|
+
* root variant (`/.`, `/..`, `//`). On Windows Git Bash, `/` spans every
|
|
698
|
+
* mounted drive, so a root-anchored `find` never terminates and accumulates
|
|
699
|
+
* as an orphaned, CPU-exhausting process (issue #2034 — #1984 was a narrow
|
|
700
|
+
* precursor fix that only patched a single call site + added a prose
|
|
701
|
+
* guardrail; this is the systemic, deterministic follow-up. Issue #2113
|
|
702
|
+
* widened the root-token match to also cover the trailing-slash/dot
|
|
703
|
+
* variants, which reach the identical whole-drive scan but were not
|
|
704
|
+
* originally matched by `FIND_ROOT_TOKEN_RE`).
|
|
691
705
|
*
|
|
692
706
|
* Scans ALL logical tokens for a `find` occurrence (not just the first word
|
|
693
707
|
* of the command), so it catches `find` appearing after `&&`, `;`, `|`, a
|
package/bin/journey.mjs
CHANGED
|
@@ -747,7 +747,8 @@ export async function preflight(ctx) {
|
|
|
747
747
|
// Act II — Forging: command symlinks + SessionStart hook (Task 6)
|
|
748
748
|
// ---------------------------------------------------------------------------
|
|
749
749
|
|
|
750
|
-
import { mkdir, symlink, readlink, lstat, readdir, rename, copyFile, readFile, writeFile, unlink } from "fs/promises";
|
|
750
|
+
import { mkdir, symlink, readlink, lstat, readdir, rename, copyFile, readFile, writeFile, unlink, rm } from "fs/promises";
|
|
751
|
+
import { compareVersions } from "./registry.mjs";
|
|
751
752
|
import { relative, dirname as pathDirname } from "path";
|
|
752
753
|
import {
|
|
753
754
|
installSessionStartHook,
|
|
@@ -1073,6 +1074,54 @@ async function copyDirIfChanged(srcDir, destDir) {
|
|
|
1073
1074
|
return { copied, unchanged };
|
|
1074
1075
|
}
|
|
1075
1076
|
|
|
1077
|
+
/**
|
|
1078
|
+
* Recursively delete entries in `destDir` that no longer exist in `srcDir`.
|
|
1079
|
+
* The mirror-image of copyDirIfChanged(): that function is additive-only (it
|
|
1080
|
+
* copies new/changed files but never removes anything), so a file dropped or
|
|
1081
|
+
* renamed upstream would otherwise linger in the persisted `~/.forge/` copy
|
|
1082
|
+
* forever (forge#2133). A missing `srcDir` or `destDir` is treated as
|
|
1083
|
+
* "nothing to reconcile" rather than an error — mirrors copyDirIfChanged()'s
|
|
1084
|
+
* own ENOENT handling.
|
|
1085
|
+
*
|
|
1086
|
+
* @param {string} srcDir
|
|
1087
|
+
* @param {string} destDir
|
|
1088
|
+
* @returns {Promise<{ removed: number }>}
|
|
1089
|
+
*/
|
|
1090
|
+
async function removeOrphans(srcDir, destDir) {
|
|
1091
|
+
let destEntries;
|
|
1092
|
+
try {
|
|
1093
|
+
destEntries = await readdir(destDir, { withFileTypes: true });
|
|
1094
|
+
} catch (err) {
|
|
1095
|
+
if (err.code === "ENOENT") return { removed: 0 };
|
|
1096
|
+
throw err;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
let srcNames = null;
|
|
1100
|
+
try {
|
|
1101
|
+
const srcEntries = await readdir(srcDir, { withFileTypes: true });
|
|
1102
|
+
srcNames = new Set(srcEntries.map((e) => e.name));
|
|
1103
|
+
} catch (err) {
|
|
1104
|
+
if (err.code !== "ENOENT") throw err;
|
|
1105
|
+
// srcDir missing entirely — every dest entry is an orphan.
|
|
1106
|
+
srcNames = new Set();
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
let removed = 0;
|
|
1110
|
+
for (const entry of destEntries) {
|
|
1111
|
+
const destPath = join(destDir, entry.name);
|
|
1112
|
+
if (!srcNames.has(entry.name)) {
|
|
1113
|
+
await rm(destPath, { recursive: true, force: true });
|
|
1114
|
+
removed++;
|
|
1115
|
+
continue;
|
|
1116
|
+
}
|
|
1117
|
+
if (entry.isDirectory()) {
|
|
1118
|
+
const sub = await removeOrphans(join(srcDir, entry.name), destPath);
|
|
1119
|
+
removed += sub.removed;
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
return { removed };
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1076
1125
|
/**
|
|
1077
1126
|
* Copy ForgeDock's own installable payload (bin/, commands/, scripts/,
|
|
1078
1127
|
* templates/) from wherever the package currently resolved — npm global
|
|
@@ -1115,6 +1164,7 @@ async function copyDirIfChanged(srcDir, destDir) {
|
|
|
1115
1164
|
* version: string,
|
|
1116
1165
|
* filesCopied?: number,
|
|
1117
1166
|
* filesUnchanged?: number,
|
|
1167
|
+
* filesRemoved?: number,
|
|
1118
1168
|
* }>}
|
|
1119
1169
|
*/
|
|
1120
1170
|
export async function persistHome(ctx) {
|
|
@@ -1142,15 +1192,55 @@ export async function persistHome(ctx) {
|
|
|
1142
1192
|
// proceed with version === ""
|
|
1143
1193
|
}
|
|
1144
1194
|
|
|
1195
|
+
// Downgrade guard (forge#2133): if a newer version is already persisted at
|
|
1196
|
+
// ~/.forge/version than the source package we're about to copy from, skip
|
|
1197
|
+
// the copy entirely rather than silently overwriting newer files with
|
|
1198
|
+
// older ones. This happens when a stale resolved package (e.g. a plain
|
|
1199
|
+
// `npx forgedock update` that resolved an old global install) runs after
|
|
1200
|
+
// ~/.forge/ was already refreshed to a newer version by some other path
|
|
1201
|
+
// (e.g. `npx forgedock@latest`). Both `version` and the persisted value
|
|
1202
|
+
// must be non-empty for the guard to apply — an unreadable/missing
|
|
1203
|
+
// ~/.forge/version is treated as "no guard needed" so first-run persist
|
|
1204
|
+
// is never blocked.
|
|
1205
|
+
const persistedVersionPath = join(persistedHome, "version");
|
|
1206
|
+
let existingPersistedVersion = "";
|
|
1207
|
+
let persistedVersionFileExists = true;
|
|
1208
|
+
try {
|
|
1209
|
+
existingPersistedVersion = readFileSync(persistedVersionPath, "utf-8").trim();
|
|
1210
|
+
} catch {
|
|
1211
|
+
// missing/unreadable — proceed, nothing to guard against. Tracked
|
|
1212
|
+
// separately from existingPersistedVersion's "" default so the
|
|
1213
|
+
// versionChanged check below (a fresh persist with no prior version
|
|
1214
|
+
// file) is still correctly treated as "changed" even when the source
|
|
1215
|
+
// package's own version also resolves to "" (e.g. missing package.json).
|
|
1216
|
+
persistedVersionFileExists = false;
|
|
1217
|
+
}
|
|
1218
|
+
if (
|
|
1219
|
+
version &&
|
|
1220
|
+
existingPersistedVersion &&
|
|
1221
|
+
compareVersions(version, existingPersistedVersion) < 0
|
|
1222
|
+
) {
|
|
1223
|
+
return {
|
|
1224
|
+
forgeHome: persistedHome,
|
|
1225
|
+
migrated: false,
|
|
1226
|
+
skipped: true,
|
|
1227
|
+
reason: `refusing to downgrade ~/.forge/ from v${existingPersistedVersion} to v${version} — source package is older than what's already persisted`,
|
|
1228
|
+
version: existingPersistedVersion,
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1145
1232
|
try {
|
|
1146
1233
|
await mkdir(persistedHome, { recursive: true });
|
|
1147
1234
|
|
|
1148
1235
|
let filesCopied = 0;
|
|
1149
1236
|
let filesUnchanged = 0;
|
|
1237
|
+
let filesRemoved = 0;
|
|
1150
1238
|
for (const name of PERSIST_HOME_DIRS) {
|
|
1151
1239
|
const res = await copyDirIfChanged(join(source, name), join(persistedHome, name));
|
|
1152
1240
|
filesCopied += res.copied;
|
|
1153
1241
|
filesUnchanged += res.unchanged;
|
|
1242
|
+
const pruned = await removeOrphans(join(source, name), join(persistedHome, name));
|
|
1243
|
+
filesRemoved += pruned.removed;
|
|
1154
1244
|
}
|
|
1155
1245
|
|
|
1156
1246
|
// Also persist package.json itself (not just PERSIST_HOME_DIRS) — several
|
|
@@ -1181,18 +1271,19 @@ export async function persistHome(ctx) {
|
|
|
1181
1271
|
|
|
1182
1272
|
// Write ~/.forge/version — content-compared like everything else here so
|
|
1183
1273
|
// an unchanged version doesn't touch the file's mtime on every re-run.
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1274
|
+
// Reuses persistedVersionPath/existingPersistedVersion read above for the
|
|
1275
|
+
// downgrade guard rather than re-reading the same file a second time. A
|
|
1276
|
+
// version file that didn't exist yet (persistedVersionFileExists: false)
|
|
1277
|
+
// always counts as "changed", regardless of whether the source's own
|
|
1278
|
+
// version happens to also be "" (e.g. missing package.json) — otherwise
|
|
1279
|
+
// "" !== "" would wrongly read as unchanged on a fresh, never-persisted
|
|
1280
|
+
// ~/.forge/.
|
|
1281
|
+
let versionChanged = !persistedVersionFileExists || existingPersistedVersion !== version;
|
|
1191
1282
|
if (versionChanged) {
|
|
1192
|
-
const tmpVersionPath =
|
|
1283
|
+
const tmpVersionPath = persistedVersionPath + ".tmp";
|
|
1193
1284
|
try {
|
|
1194
1285
|
writeFileSync(tmpVersionPath, version + "\n", "utf-8");
|
|
1195
|
-
renameSync(tmpVersionPath,
|
|
1286
|
+
renameSync(tmpVersionPath, persistedVersionPath);
|
|
1196
1287
|
} catch (err) {
|
|
1197
1288
|
try { unlinkSync(tmpVersionPath); } catch { /* best-effort cleanup */ }
|
|
1198
1289
|
throw err;
|
|
@@ -1201,11 +1292,12 @@ export async function persistHome(ctx) {
|
|
|
1201
1292
|
|
|
1202
1293
|
return {
|
|
1203
1294
|
forgeHome: persistedHome,
|
|
1204
|
-
migrated: filesCopied > 0 || versionChanged,
|
|
1295
|
+
migrated: filesCopied > 0 || filesRemoved > 0 || versionChanged,
|
|
1205
1296
|
skipped: false,
|
|
1206
1297
|
version,
|
|
1207
1298
|
filesCopied,
|
|
1208
1299
|
filesUnchanged,
|
|
1300
|
+
filesRemoved,
|
|
1209
1301
|
};
|
|
1210
1302
|
} catch (err) {
|
|
1211
1303
|
// Fail-open (forge#383): a permission error, disk-full, etc. must never
|
package/commands/forge-stats.md
CHANGED
|
@@ -34,8 +34,8 @@ Commands are classified by role. Each role has a max line count — beyond this,
|
|
|
34
34
|
|------|-----------|-----------|
|
|
35
35
|
| Orchestrator (work-on, orchestrate, review-pr) | 800 | Delegates to sub-agents; needs full pipeline logic |
|
|
36
36
|
| Agent catalog (review-pr-agents, review-pr-staging) | 1000 | Reference file read on demand, not fed whole to one agent |
|
|
37
|
-
| Specialist (milestone, analytics, validate, etc.) | 400 | Single-purpose; Sonnet attention degrades past this |
|
|
38
|
-
| Utility (cleanup,
|
|
37
|
+
| Specialist (milestone, analytics, sync-ecosystem, validate, etc.) | 400 | Single-purpose or multi-subcommand; Sonnet attention degrades past this |
|
|
38
|
+
| Utility (cleanup, forge-stats) | 250 | Simple tasks; should stay lean |
|
|
39
39
|
| Sub-file (quality-gate, pipeline-health) | 350 | Invoked by parent; shares context budget |
|
|
40
40
|
|
|
41
41
|
**Current overages (v1.1.0 baseline):**
|
|
@@ -46,7 +46,8 @@ Commands are classified by role. Each role has a max line count — beyond this,
|
|
|
46
46
|
- `review-pr.md`: 796 lines (threshold 800) — OK
|
|
47
47
|
- `validate.md`: 97 lines (threshold 400) — OK (compressed from 679)
|
|
48
48
|
- `orchestrate.md`: 654 lines (threshold 800) — OK
|
|
49
|
-
- `analytics.md`:
|
|
49
|
+
- `analytics.md`: 856 lines (threshold 400) — **114% over**
|
|
50
|
+
- `sync-ecosystem.md`: 799 lines (threshold 400) — **99% over**
|
|
50
51
|
|
|
51
52
|
---
|
|
52
53
|
|
|
@@ -75,7 +76,7 @@ for cmd in commands/*.md; do
|
|
|
75
76
|
case "$name" in
|
|
76
77
|
work-on.md|orchestrate.md|review-pr.md) threshold=800 ;;
|
|
77
78
|
review-pr-agents.md|review-pr-staging.md) threshold=1000 ;;
|
|
78
|
-
cleanup.md|
|
|
79
|
+
cleanup.md|forge-stats.md) threshold=250 ;;
|
|
79
80
|
quality-gate.md|pipeline-health.md) threshold=350 ;;
|
|
80
81
|
*) threshold=400 ;;
|
|
81
82
|
esac
|
package/commands/issue.md
CHANGED
|
@@ -13,7 +13,7 @@ You create GitHub issues with the exact structure the `/work-on` pipeline expect
|
|
|
13
13
|
|
|
14
14
|
`/issue` is the structured create-hook for issue creation across the pipeline — it enforces the canonical template, reads code before drafting, runs dedup, and validates mandatory sections. Because those checks are deterministic, `/issue` **creates the issue by default once they pass** — it does not wait for a human to approve the draft. Pass `--dry-run` to review the draft without creating anything (see Argument Parsing below).
|
|
15
15
|
|
|
16
|
-
`/issue` also accepts a **programmatic invocation form** for callers that have already composed a title, body, labels, and (optionally) a milestone — e.g. `Skill(skill="issue", args="--title \"fix: ...\" --body-file
|
|
16
|
+
`/issue` also accepts a **programmatic invocation form** for callers that have already composed a title, body, labels, and (optionally) a milestone — e.g. `Skill(skill="issue", args="--title \"fix: ...\" --body-file \"$(mktemp)\" --label bug --label P2")` (the caller writes the body to that path before invoking). This form skips the free-text parsing (Phase 1) and LLM drafting (Phase 3) entirely, but still runs the same dedup and body-validation correctness gates as the interactive path. See **Programmatic Invocation Contract** below — in particular, `--body-file` callers MUST use a `mktemp`-created path, never a fixed literal like `/tmp/body.md`; concurrent orchestrated agents share this host's `/tmp`, and a fixed path can be clobbered mid-flight by another agent (forge#2198).
|
|
17
17
|
|
|
18
18
|
**Agent model policy**: `model: "{DEFAULT_MODEL}"` — resolved from forge.yaml `agents.default_model`, else "sonnet" (standard tier). Fallback: `model: "opus"` if rate-limited. Feature gate: pass `effort` in Task/Skill spawns only on Claude Code >= 2.1.154.
|
|
19
19
|
**NEVER use plan mode (EnterPlanMode).**
|
|
@@ -181,6 +181,10 @@ For callers that have already composed a title, body, labels, and (optionally) a
|
|
|
181
181
|
|
|
182
182
|
If `--title` is present but neither `--body` nor `--body-file` is supplied, or both are supplied, this is a usage error: print `ERROR: programmatic mode requires exactly one of --body or --body-file` and STOP — do not fall through to Phase 2D or Phase 4B.
|
|
183
183
|
|
|
184
|
+
**`--body-file` path requirement (MANDATORY for the caller)**: The path passed to `--body-file` MUST come from `mktemp` (e.g. `BODY_FILE="$(mktemp)"`), never a fixed literal such as `/tmp/body.md` or `/tmp/issue.json`. Concurrent orchestrated agents (`/orchestrate` dispatches up to `orchestration.max_concurrent` agents, default 12) share this host's `/tmp` — a fixed filename collides across agents, and a second agent's write to the same path can silently corrupt the first agent's staged content before it is read. `mktemp` gives every caller a collision-free path at effectively zero cost. This applies to every `--body-file` caller across the pipeline (`commands/work-on.md`, `commands/work-on/decompose.md`, `commands/review-pr*.md`, `commands/orchestrate/phase-1-resolve.md`, and all others) — all current callers already comply; this is the stated contract that keeps it that way (forge#2198).
|
|
185
|
+
|
|
186
|
+
**Fail-loud read ordering (the defensive mechanism against a clobbered body-file)**: `/issue` reads `$PROGRAMMATIC_BODY_FILE` exactly once, into memory, immediately below — it never re-reads the file later in the pipeline. This is deliberate: combined with a `mktemp`-unique path, it minimizes the window in which another process could overwrite the file between the caller's write and this read to effectively nil (no other agent has any reason to target a `mktemp`-generated path). If the file is missing or unreadable at read time, the check below fails loudly (`exit 1`) rather than silently proceeding with an empty or partial body — a clobbered/missing body-file is surfaced as an error, not pushed to GitHub.
|
|
187
|
+
|
|
184
188
|
```bash
|
|
185
189
|
if [ "$PROGRAMMATIC_MODE" = "true" ]; then
|
|
186
190
|
if [ -n "$PROGRAMMATIC_BODY" ] && [ -n "$PROGRAMMATIC_BODY_FILE" ]; then
|
|
@@ -425,6 +425,8 @@ Agent(
|
|
|
425
425
|
- For satellite repo issues: `Skill(skill='work-on', args='{SATELLITE_PREFIX}:{NUMBER} --under-orchestration')` (prefix from forge.yaml → repos.satellites)
|
|
426
426
|
- The `--under-orchestration` flag tells `/work-on` to post its phase-entry `FORGE:HEARTBEAT` comments (Phases 0/1/3/5) — this orchestrator's Step 4B.5 stall detector depends on those timestamps. A solo `/work-on` run omits the flag and skips those writes entirely (see `commands/work-on.md` → Orchestration Flag).
|
|
427
427
|
- NEVER bypass /work-on with manual git/gh commands — the label updates and structured comments are critical for tracking
|
|
428
|
+
- **Temp files — ALWAYS use `mktemp`, NEVER a fixed path**: You are one of several agents running concurrently on this host and you share its `/tmp` with all of them. Any time you stage content in a temp file before passing it to `gh` (e.g. `--body-file`), create that path with `mktemp` (e.g. `BODY_FILE="$(mktemp)"`) — never a fixed literal like `/tmp/body.md` or `/tmp/issue.json`. A fixed path collides with another concurrently-running agent and can silently overwrite the content you staged (or you can silently overwrite theirs) before either of you reads it back — this has caused real cross-agent GitHub issue-body corruption. `mktemp` costs nothing and eliminates the collision. (forge#2198)
|
|
429
|
+
- **`docker cp` — NEVER write into a bind-mounted shared container**: If the project you're working on runs its dev/test containers with a bind mount to the main (non-worktree) checkout, `docker cp` into that container writes through the mount into the main checkout — not your isolated per-issue worktree. Before running `docker cp` into any container, confirm its mount source is your own worktree, not a shared/main one; if you can't confirm that, don't assume an in-container test run reflects your feature branch. (forge#2198)
|
|
428
430
|
- NEVER target `main` for PRs targeting the default repo. Use `{STAGING_BRANCH}` for fast-lane issues, or `milestone/{slug}` for milestone issues.
|
|
429
431
|
- Satellite repos (MCP, n8n) have no staging branch — fast-lane PRs go to `main` for those.
|
|
430
432
|
- If the issue is INVALID after investigation, close it with a comment explaining why
|
package/commands/qa-sweep.md
CHANGED
|
@@ -36,6 +36,23 @@ See README.md Requirements section for full setup instructions.
|
|
|
36
36
|
|
|
37
37
|
---
|
|
38
38
|
|
|
39
|
+
## Playwright MCP Prerequisite (MANDATORY — check before proceeding)
|
|
40
|
+
|
|
41
|
+
Playwright MCP is a **guaranteed ForgeDock dependency** for all browser automation commands. Before running any qa-sweep phase, verify it is available:
|
|
42
|
+
|
|
43
|
+
**Check**: Confirm `mcp__playwright__*` tools are available in your tool context (e.g., `mcp__playwright__browser_navigate`, `mcp__playwright__browser_snapshot`).
|
|
44
|
+
|
|
45
|
+
**If Playwright MCP tools are NOT available**: STOP immediately. Do not attempt to run qa-sweep — all browser automation steps will fail silently.
|
|
46
|
+
|
|
47
|
+
**Fix**:
|
|
48
|
+
1. Register Playwright MCP: `claude mcp add playwright npx @playwright/mcp@latest`
|
|
49
|
+
2. Restart Claude Code to reload MCP servers
|
|
50
|
+
3. Verify with: `npx forgedock doctor` (Check 9 confirms registration)
|
|
51
|
+
|
|
52
|
+
See README.md Requirements section for full setup instructions.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
39
56
|
## Browser Tool Reference
|
|
40
57
|
|
|
41
58
|
All browser automation uses Playwright MCP tools (`mcp__playwright__*`). Playwright MCP is a guaranteed ForgeDock dependency — see the Playwright MCP Prerequisite section above.
|
package/commands/replay.md
CHANGED
|
@@ -95,7 +95,7 @@ fi
|
|
|
95
95
|
|
|
96
96
|
```bash
|
|
97
97
|
# Filter to comments that contain a FORGE pipeline annotation
|
|
98
|
-
FORGE_COMMENTS=$(echo "$ALL_COMMENTS" | jq -c '[.[] | select(.body | test("<!-- FORGE:(INVESTIGATOR|CONTRACT|CONTEXT|ARCHITECT|BUILDER|DECOMPOSED|TRAJECTORY)"))]')
|
|
98
|
+
FORGE_COMMENTS=$(echo "$ALL_COMMENTS" | jq -c '[.[] | select(.body | test("<!-- FORGE:(INVESTIGATOR|CONTRACT|CONTEXT|ARCHITECT|BUILDER|DECOMPOSED|SUMMARY|TRAJECTORY)"))]')
|
|
99
99
|
FORGE_COUNT=$(echo "$FORGE_COMMENTS" | jq 'length')
|
|
100
100
|
|
|
101
101
|
if [ "$FORGE_COUNT" -eq 0 ]; then
|
|
@@ -115,7 +115,7 @@ echo ""
|
|
|
115
115
|
|
|
116
116
|
## Step 5: Display Each Annotation
|
|
117
117
|
|
|
118
|
-
Phase mapping (inline): `INVESTIGATOR`→Phase 1, `CONTRACT`→Phase 3C, `CONTEXT`→Phase 3C.5, `ARCHITECT`→Phase 3C.6, `BUILDER`→Phase 3M, `DECOMPOSED`→Phase 2, `TRAJECTORY`→Phase 7.
|
|
118
|
+
Phase mapping (inline): `INVESTIGATOR`→Phase 1, `CONTRACT`→Phase 3C, `CONTEXT`→Phase 3C.5, `ARCHITECT`→Phase 3C.6, `BUILDER`→Phase 3M, `DECOMPOSED`→Phase 2, `SUMMARY`→Phase C4.5, `TRAJECTORY`→Phase 7.
|
|
119
119
|
|
|
120
120
|
For each FORGE comment in chronological order, print a formatted block. Use Python for robust multiline extraction from the comment body.
|
|
121
121
|
|
|
@@ -136,6 +136,7 @@ echo "$FORGE_COMMENTS" | jq -c '.[]' | while IFS= read -r COMMENT; do
|
|
|
136
136
|
"FORGE:ARCHITECT") PHASE_LABEL="Phase 3C.6 — Architecture Plan" ;;
|
|
137
137
|
"FORGE:BUILDER") PHASE_LABEL="Phase 3M — Implementation" ;;
|
|
138
138
|
"FORGE:DECOMPOSED") PHASE_LABEL="Phase 2 — Decomposition" ;;
|
|
139
|
+
"FORGE:SUMMARY") PHASE_LABEL="Phase C4.5 — Shareable Summary" ;;
|
|
139
140
|
"FORGE:TRAJECTORY") PHASE_LABEL="Phase 7 — Pipeline Trajectory" ;;
|
|
140
141
|
*) PHASE_LABEL="$ANNOTATION" ;;
|
|
141
142
|
esac
|
|
@@ -216,6 +217,19 @@ elif annotation == "FORGE:DECOMPOSED":
|
|
|
216
217
|
for s in subs[:5]:
|
|
217
218
|
print(f"│ {s}")
|
|
218
219
|
|
|
220
|
+
elif annotation == "FORGE:SUMMARY":
|
|
221
|
+
lane = extract(r'\*\*Lane\*\*\s*\|\s*([^\|]+)')
|
|
222
|
+
verdict = extract(r'\*\*Investigation verdict\*\*\s*\|\s*([^\|]+)')
|
|
223
|
+
task_type = extract(r'\*\*Task type\*\*\s*\|\s*([^\|]+)')
|
|
224
|
+
review = extract(r'\*\*Review verdict\*\*\s*\|\s*([^\|]+)')
|
|
225
|
+
files = extract(r'\*\*Files changed\*\*\s*\|\s*([^\|]+)')
|
|
226
|
+
pr_link = extract(r'\[View PR #(\d+)\]')
|
|
227
|
+
print(f"│ Lane : {lane.strip()[:60]}")
|
|
228
|
+
print(f"│ Verdict : {verdict.strip()[:60]}")
|
|
229
|
+
print(f"│ Task type : {task_type.strip()[:60]}")
|
|
230
|
+
print(f"│ Review : {review.strip()[:60]}")
|
|
231
|
+
print(f"│ Files : {files.strip()[:30]} | PR: #{pr_link}")
|
|
232
|
+
|
|
219
233
|
elif annotation == "FORGE:TRAJECTORY":
|
|
220
234
|
# Print the phase table lines verbatim (they're already formatted)
|
|
221
235
|
in_table = False
|