forgedock 1.1.1 → 1.1.2
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 +53 -8
- package/bin/batch-runner.mjs +339 -0
- package/bin/demo.mjs +92 -7
- package/bin/engine/phases.mjs +20 -0
- package/bin/engine/state.mjs +8 -3
- package/bin/engine-cli.mjs +177 -5
- package/bin/forgedock.mjs +409 -26
- package/bin/hooks/interactive-engine.mjs +509 -0
- package/bin/hooks/pre-tool-use.mjs +498 -0
- package/bin/hooks/subagent-stop-enforce.mjs +197 -0
- package/bin/journey.mjs +72 -14
- package/bin/registry.mjs +139 -0
- package/bin/report.mjs +447 -0
- package/bin/runner.mjs +68 -19
- package/bin/settings-hook.mjs +235 -1
- package/commands/adopt.md +4 -1
- package/commands/analytics.md +4 -1
- package/commands/audit-agents.md +110 -27
- package/commands/audit.md +20 -1
- package/commands/autopilot.md +80 -8
- package/commands/changelog.md +2 -1
- package/commands/ci-audit.md +2 -1
- package/commands/cleanup.md +161 -1
- package/commands/compat-audit.md +2 -1
- package/commands/deploy-info.md +58 -1
- package/commands/design-bench.md +5 -1
- package/commands/design-render-critique-loop.md +1 -1
- package/commands/diagnose.md +2 -1
- package/commands/explain.md +2 -1
- package/commands/forge-stats.md +121 -2
- package/commands/forgedock-init.md +1 -1
- package/commands/geo-audit.md +3 -0
- package/commands/incident-response.md +2 -1
- package/commands/issue.md +23 -2
- package/commands/milestone.md +77 -12
- package/commands/optimize.md +2 -1
- package/commands/orchestrate/config.md +67 -0
- package/commands/orchestrate/phase-1-resolve.md +159 -0
- package/commands/orchestrate/phase-2-triage.md +126 -0
- package/commands/orchestrate/phase-2.5-synthesis.md +156 -0
- package/commands/orchestrate/phase-3-dependency.md +591 -0
- package/commands/orchestrate/phase-4-execution.md +962 -0
- package/commands/orchestrate/phase-5-cleanup.md +47 -0
- package/commands/orchestrate/phase-6-report.md +198 -0
- package/commands/orchestrate/safety.md +62 -0
- package/commands/orchestrate.md +25 -2045
- package/commands/pipeline-health/config.md +34 -0
- package/commands/pipeline-health/phase-1-context.md +180 -0
- package/commands/pipeline-health/phase-2-metrics.md +1729 -0
- package/commands/pipeline-health/phase-3-analyze.md +204 -0
- package/commands/pipeline-health/phase-4-proposals.md +117 -0
- package/commands/pipeline-health/phase-5-report.md +284 -0
- package/commands/pipeline-health/phase-6-summary.md +52 -0
- package/commands/pipeline-health.md +25 -2401
- package/commands/pipeline-resume.md +24 -1
- package/commands/pipeline-status.md +65 -0
- package/commands/qa-sweep.md +4 -1
- package/commands/quality-gate.md +248 -10
- package/commands/replay.md +2 -1
- package/commands/review-pr-agents/api.md +135 -0
- package/commands/review-pr-agents/auth.md +137 -0
- package/commands/review-pr-agents/billing.md +101 -0
- package/commands/review-pr-agents/concurrency.md +145 -0
- package/commands/review-pr-agents/database.md +103 -0
- package/commands/review-pr-agents/frontend.md +89 -0
- package/commands/review-pr-agents/infra.md +385 -0
- package/commands/review-pr-agents/protocols.md +185 -0
- package/commands/review-pr-agents/scraper.md +185 -0
- package/commands/review-pr-agents/security.md +406 -0
- package/commands/review-pr-agents.md +45 -1722
- package/commands/review-pr-staging.md +196 -40
- package/commands/review-pr.md +591 -52
- package/commands/rollback.md +2 -1
- package/commands/scope.md +2 -1
- package/commands/security-audit.md +2 -1
- package/commands/signal-planner.md +623 -0
- package/commands/sync-ecosystem.md +1 -0
- package/commands/test-gate.md +17 -1
- package/commands/validate.md +2 -1
- package/commands/work-on/build/architect.md +28 -0
- package/commands/work-on/build/context.md +2 -0
- package/commands/work-on/build/implement.md +3 -1
- package/commands/work-on/build/validate.md +26 -7
- package/commands/work-on/build.md +163 -24
- package/commands/work-on/close.md +130 -3
- package/commands/work-on/decompose.md +19 -1
- package/commands/work-on/investigate.md +117 -2
- package/commands/work-on/review.md +24 -4
- package/commands/work-on-monolithic.md +7 -2
- package/commands/work-on.md +202 -4
- package/docs/BRAND.md +14 -7
- package/docs/CI.md +2 -2
- package/docs/CODEX.md +36 -1
- package/docs/CONFIG.md +5 -8
- package/docs/FORGE-PROTOCOL.md +15 -1064
- package/docs/anthropic-devrel-outreach.md +2 -2
- package/docs/articles/command-decomposition-token-savings.md +214 -0
- package/docs/articles/model-release-playbook.md +131 -0
- package/docs/articles/per-repo-adaptive-scripts-token-savings.md +27 -25
- package/docs/claude-breakpoints.json +75 -5
- package/docs/design/abc-benchmark.md +1 -1
- package/docs/design/design-spec-schema.md +1 -1
- package/docs/design/divergent-generation.md +1 -1
- package/docs/design/eval-gate-runbook.md +178 -0
- package/docs/design/show-hn-draft.md +26 -1
- package/docs/design/token-cost-baseline-1298.md +59 -0
- package/docs/eval/README.md +117 -0
- package/docs/eval/corpus/seed-v1.json +6 -0
- package/docs/eval/published/claude-sonnet-4-6-2026-07-04.json +33 -0
- package/docs/site/annotations-explained.md +24 -0
- package/docs/site/command-learning-path.md +51 -14
- package/docs/site/command-reference.md +339 -113
- package/docs/site/for-companies.md +95 -0
- package/docs/site/forge-annotation-protocol.md +12 -256
- package/docs/site/getting-started.md +43 -4
- package/docs/site/github-is-the-memory.md +113 -0
- package/docs/site/how-it-works.md +3 -13
- package/docs/site/index.md +9 -1
- package/docs/spec/eval-run-result.md +147 -0
- package/docs/spec/forge-protocol-v1.md +441 -18
- package/docs/spec/label-state-machine.md +55 -0
- package/docs/spec/review-protocol.md +158 -0
- package/docs/spec-graph-schema.md +28 -0
- package/docs/superpowers/specs/2026-07-04-economic-self-governance-design.md +181 -0
- package/docs/superpowers/specs/2026-07-04-provenance-trail-design.md +189 -0
- package/examples/forgedock-demo/README.md +41 -22
- package/examples/forgedock-demo/issues/06-bug-tags-type-validation.md +43 -0
- package/examples/forgedock-demo/issues/07-bug-negative-offset-pagination.md +42 -0
- package/examples/forgedock-demo/issues/08-bug-patch-mass-assignment.md +49 -0
- package/examples/forgedock-demo/issues/09-bug-count-injection.md +47 -0
- package/examples/forgedock-demo/issues/10-feature-sort-param.md +39 -0
- package/examples/forgedock-demo/issues/11-feature-patch-tags.md +37 -0
- package/examples/forgedock-demo/issues/12-feature-bulk-create.md +42 -0
- package/examples/forgedock-demo/issues/13-feature-archive-notes.md +46 -0
- package/examples/forgedock-demo/issues/14-refactor-extract-title-validation.md +41 -0
- package/examples/forgedock-demo/issues/15-refactor-protected-route-flag.md +43 -0
- package/examples/forgedock-demo/issues/16-refactor-extract-store.md +45 -0
- package/examples/forgedock-demo/issues/17-perf-tags-dedup-set.md +35 -0
- package/examples/forgedock-demo/issues/18-perf-remove-redundant-sort.md +37 -0
- package/examples/forgedock-demo/issues/19-docs-api-reference-full.md +53 -0
- package/examples/forgedock-demo/issues/20-docs-intentional-flaw-convention.md +46 -0
- package/examples/forgedock-demo/scripts/smoke.js +18 -1
- package/examples/forgedock-demo/src/db.js +36 -11
- package/examples/forgedock-demo/src/routes/notes.js +106 -9
- package/examples/forgedock-demo/src/server.js +4 -1
- package/package.json +3 -1
- package/packages/protocol/LICENSE +21 -0
- package/packages/protocol/src/cli.js +135 -0
- package/packages/protocol/src/emit.js +146 -0
- package/packages/protocol/src/index.js +43 -0
- package/packages/protocol/src/parse.js +229 -0
- package/packages/protocol/src/types.js +171 -0
- package/packages/protocol/src/validate.js +122 -0
- package/scripts/bench-topology-cost.mjs +372 -0
- package/scripts/check-command-docs-drift.sh +198 -0
- package/scripts/check-protocol-restatements.sh +176 -0
- package/scripts/check-registry/README.md +72 -0
- package/scripts/check-registry/manifest.json +6 -0
- package/scripts/code-index.sh +454 -0
- package/scripts/conformance-check.mjs +168 -0
- package/scripts/doctor-pipeline-state.sh +484 -0
- package/scripts/eval-gate-baseline.json +17 -0
- package/scripts/eval-gate-scorecard.mjs +242 -0
- package/scripts/eval-scorecard-aggregator.mjs +252 -0
- package/scripts/flaky-quarantine.sh +241 -0
- package/scripts/forge-annotation.sh +567 -0
- package/scripts/issue-dedup.sh +236 -0
- package/scripts/validate-annotation-node.mjs +233 -0
- package/scripts/worktree-lifecycle.sh +3 -3
- package/templates/devdocs/agent/using-forgedock.md +14 -0
- package/docs/site/.vitepress/config.mjs +0 -85
package/bin/forgedock.mjs
CHANGED
|
@@ -27,8 +27,20 @@ import {
|
|
|
27
27
|
backupExisting,
|
|
28
28
|
manualLowConfidenceKeys,
|
|
29
29
|
} from "./journey.mjs";
|
|
30
|
-
import {
|
|
31
|
-
|
|
30
|
+
import {
|
|
31
|
+
removeSessionStartHook,
|
|
32
|
+
removeSubagentStopHook,
|
|
33
|
+
removePreToolUseHook,
|
|
34
|
+
removeSubagentStopEnforceHook,
|
|
35
|
+
} from "./settings-hook.mjs";
|
|
36
|
+
import {
|
|
37
|
+
resolveState,
|
|
38
|
+
setOptOut,
|
|
39
|
+
clearNudgeSeen,
|
|
40
|
+
detectClaudeVersion,
|
|
41
|
+
loadBreakpoints,
|
|
42
|
+
hasFeature,
|
|
43
|
+
} from "./registry.mjs";
|
|
32
44
|
import { renderMark, ember } from "./cinema.mjs";
|
|
33
45
|
import {
|
|
34
46
|
renderLogo,
|
|
@@ -58,10 +70,54 @@ const COMMANDS_DIR = join(FORGE_HOME, "commands");
|
|
|
58
70
|
// os.homedir() as the always-available fallback (no hard exit — see #744).
|
|
59
71
|
const HOME = process.env.HOME || process.env.USERPROFILE || homedir();
|
|
60
72
|
|
|
61
|
-
|
|
62
|
-
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// Install-mode path resolution
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// ForgeDock installs commands globally, to ~/.claude/. That is the only
|
|
77
|
+
// location the installer (bin/journey.mjs: forge()) ever writes to — it
|
|
78
|
+
// resolves its target from ctx.home (HOME/USERPROFILE/os.homedir()), never
|
|
79
|
+
// from process.cwd(). A prior refactor (#1288/#1500) added a project-scoped-
|
|
80
|
+
// by-default detection layer here without updating the installer to match,
|
|
81
|
+
// producing a split-brain where status/doctor/uninstall could report a
|
|
82
|
+
// "project-scoped" location that install() never actually wrote to (see
|
|
83
|
+
// #1589). This was backed out: detection now always agrees with what the
|
|
84
|
+
// installer does.
|
|
85
|
+
//
|
|
86
|
+
// --global is still accepted as a CLI flag for backward compatibility with
|
|
87
|
+
// existing invocations, but it is now a no-op — global is the only supported
|
|
88
|
+
// install location.
|
|
89
|
+
|
|
90
|
+
const GLOBAL_CLAUDE_DIR = join(HOME, ".claude");
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Resolve the install target directories for the current invocation.
|
|
94
|
+
* Always resolves to the global (~/.claude) location — see the block
|
|
95
|
+
* comment above for why project-scoped resolution was removed.
|
|
96
|
+
*
|
|
97
|
+
* @returns {{ targetDir: string, scriptsTargetDir: string, manifestPath: string, isGlobal: boolean }}
|
|
98
|
+
*/
|
|
99
|
+
function resolveInstallPaths() {
|
|
100
|
+
return {
|
|
101
|
+
targetDir: join(GLOBAL_CLAUDE_DIR, "commands"),
|
|
102
|
+
scriptsTargetDir: join(GLOBAL_CLAUDE_DIR, "scripts"),
|
|
103
|
+
manifestPath: join(GLOBAL_CLAUDE_DIR, "forgedock", "copied-commands.json"),
|
|
104
|
+
isGlobal: true,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Resolve install paths for uninstall/update/doctor/status. Kept as a
|
|
110
|
+
* distinct entry point (rather than calling resolveInstallPaths() directly)
|
|
111
|
+
* since these commands conceptually "detect" an existing install; today
|
|
112
|
+
* that always resolves to the same global location as resolveInstallPaths().
|
|
113
|
+
*
|
|
114
|
+
* @returns {{ targetDir: string, scriptsTargetDir: string, manifestPath: string, isGlobal: boolean }}
|
|
115
|
+
*/
|
|
116
|
+
function detectInstallPaths() {
|
|
117
|
+
return resolveInstallPaths();
|
|
118
|
+
}
|
|
119
|
+
|
|
63
120
|
const SCRIPTS_DIR = join(FORGE_HOME, "scripts");
|
|
64
|
-
const SCRIPTS_TARGET_DIR = join(HOME, ".claude", "scripts");
|
|
65
121
|
|
|
66
122
|
/**
|
|
67
123
|
* Allowlist of pipeline-agent scripts that `uninstall` cleans up from
|
|
@@ -84,12 +140,15 @@ const PIPELINE_SCRIPTS = new Set([
|
|
|
84
140
|
// --minimal is init-only. Subcommands with their own flag parsing (run,
|
|
85
141
|
// run-issue, demo) receive restArgs — everything after the command token.
|
|
86
142
|
const rawArgs = process.argv.slice(2);
|
|
87
|
-
const FLAGS = new Set(["--fast", "--manual", "--verbose", "--minimal"]);
|
|
143
|
+
const FLAGS = new Set(["--fast", "--manual", "--verbose", "--minimal", "--extras"]);
|
|
88
144
|
const flags = rawArgs.filter((a) => FLAGS.has(a));
|
|
89
|
-
|
|
145
|
+
// --global is stripped from positionals/restArgs for backward compatibility
|
|
146
|
+
// with existing invocations, but is otherwise a no-op — see the install-mode
|
|
147
|
+
// path resolution block above (global is now the only install location).
|
|
148
|
+
const positional = rawArgs.filter((a) => !FLAGS.has(a) && a !== "--global");
|
|
90
149
|
const command = positional[0] || "install";
|
|
91
|
-
const cmdIdx = rawArgs.findIndex((a) => !FLAGS.has(a));
|
|
92
|
-
const restArgs = cmdIdx === -1 ? [] : rawArgs.slice(cmdIdx + 1);
|
|
150
|
+
const cmdIdx = rawArgs.findIndex((a) => !FLAGS.has(a) && a !== "--global");
|
|
151
|
+
const restArgs = cmdIdx === -1 ? [] : rawArgs.slice(cmdIdx + 1).filter((a) => a !== "--global");
|
|
93
152
|
|
|
94
153
|
// ---------------------------------------------------------------------------
|
|
95
154
|
// SessionStart hook — settings.json helpers
|
|
@@ -492,6 +551,11 @@ function ctx() {
|
|
|
492
551
|
const c = makeCtx({ argv: flags });
|
|
493
552
|
c.forgeHome = FORGE_HOME;
|
|
494
553
|
c.home = HOME;
|
|
554
|
+
c.includeExtras = flags.includes("--extras");
|
|
555
|
+
// Detect installed Claude Code version for version-gated install paths (#1252).
|
|
556
|
+
// Fail-open: null means version is unknown; callers must handle gracefully.
|
|
557
|
+
c.claudeVersion = detectClaudeVersion();
|
|
558
|
+
c.breakpoints = loadBreakpoints(FORGE_HOME);
|
|
495
559
|
return c;
|
|
496
560
|
}
|
|
497
561
|
|
|
@@ -506,7 +570,8 @@ function statusScreen(c) {
|
|
|
506
570
|
const configured = existsSync(join(c.cwd, "forge.yaml"));
|
|
507
571
|
c.stdout.write(` directory ${state}\n`);
|
|
508
572
|
c.stdout.write(` forge.yaml ${configured ? "present" : "missing"}\n`);
|
|
509
|
-
|
|
573
|
+
const { targetDir: statusTargetDir } = detectInstallPaths();
|
|
574
|
+
c.stdout.write(` commands ${existsSync(statusTargetDir) ? "installed at " + statusTargetDir : "not installed"}\n`);
|
|
510
575
|
if (state === "managed-optedout") {
|
|
511
576
|
c.stdout.write(`\n ForgeDock is disabled (opted out) here. Re-enable: npx forgedock enable\n`);
|
|
512
577
|
} else if (state === "unmanaged") {
|
|
@@ -546,7 +611,7 @@ async function initFlow(c) {
|
|
|
546
611
|
stagingBranch: draft.branches.staging.value,
|
|
547
612
|
});
|
|
548
613
|
atomicWriteFile(outputPath, content);
|
|
549
|
-
celebrate(c, { written: true, todoCount: 0 });
|
|
614
|
+
celebrate(c, { written: true, todoCount: 0, isMinimal: true });
|
|
550
615
|
return 0;
|
|
551
616
|
}
|
|
552
617
|
|
|
@@ -581,6 +646,10 @@ async function uninstall() {
|
|
|
581
646
|
console.log(`${BOLD}ForgeDock${RESET} — Removing pipeline commands`);
|
|
582
647
|
console.log("");
|
|
583
648
|
|
|
649
|
+
const { targetDir, scriptsTargetDir, manifestPath } = detectInstallPaths();
|
|
650
|
+
console.log(` Mode: global (~/.claude)`);
|
|
651
|
+
console.log("");
|
|
652
|
+
|
|
584
653
|
const files = await findMarkdownFiles(COMMANDS_DIR);
|
|
585
654
|
let removed = 0;
|
|
586
655
|
|
|
@@ -591,7 +660,7 @@ async function uninstall() {
|
|
|
591
660
|
// installs that predate the manifest.
|
|
592
661
|
let manifest = null;
|
|
593
662
|
try {
|
|
594
|
-
manifest = JSON.parse(await readFile(
|
|
663
|
+
manifest = JSON.parse(await readFile(manifestPath, "utf-8"));
|
|
595
664
|
} catch {
|
|
596
665
|
manifest = null;
|
|
597
666
|
}
|
|
@@ -602,7 +671,7 @@ async function uninstall() {
|
|
|
602
671
|
|
|
603
672
|
for (const file of files) {
|
|
604
673
|
const rel = relative(COMMANDS_DIR, file);
|
|
605
|
-
const target = join(
|
|
674
|
+
const target = join(targetDir, rel);
|
|
606
675
|
|
|
607
676
|
try {
|
|
608
677
|
const stats = await lstat(target);
|
|
@@ -640,7 +709,7 @@ async function uninstall() {
|
|
|
640
709
|
// them — the manifest is the only record of ForgeDock's ownership over them.
|
|
641
710
|
const manifestRels = Object.keys(manifestFiles);
|
|
642
711
|
for (const rel of manifestRels) {
|
|
643
|
-
const target = join(
|
|
712
|
+
const target = join(targetDir, rel);
|
|
644
713
|
try {
|
|
645
714
|
const stats = await lstat(target);
|
|
646
715
|
if (stats.isFile() && !stats.isSymbolicLink()) {
|
|
@@ -661,8 +730,8 @@ async function uninstall() {
|
|
|
661
730
|
if (manifest && manifestRels.length > 0) {
|
|
662
731
|
manifest.files = {};
|
|
663
732
|
try {
|
|
664
|
-
await mkdir(dirname(
|
|
665
|
-
await writeFile(
|
|
733
|
+
await mkdir(dirname(manifestPath), { recursive: true });
|
|
734
|
+
await writeFile(manifestPath, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
|
|
666
735
|
} catch {
|
|
667
736
|
// Best-effort — a failed manifest write is non-fatal
|
|
668
737
|
}
|
|
@@ -672,13 +741,13 @@ async function uninstall() {
|
|
|
672
741
|
console.log(`Done. Removed: ${removed} commands.`);
|
|
673
742
|
console.log("");
|
|
674
743
|
|
|
675
|
-
// Remove scripts installed by ForgeDock from
|
|
744
|
+
// Remove scripts installed by ForgeDock from the detected scripts target dir
|
|
676
745
|
const scriptFiles = await findScriptFiles(SCRIPTS_DIR);
|
|
677
746
|
let scriptsRemoved = 0;
|
|
678
747
|
|
|
679
748
|
for (const file of scriptFiles) {
|
|
680
749
|
const rel = relative(SCRIPTS_DIR, file);
|
|
681
|
-
const target = join(
|
|
750
|
+
const target = join(scriptsTargetDir, rel);
|
|
682
751
|
|
|
683
752
|
try {
|
|
684
753
|
const stats = await lstat(target);
|
|
@@ -711,9 +780,8 @@ async function uninstall() {
|
|
|
711
780
|
|
|
712
781
|
// Remove the SessionStart hook from ~/.claude/settings.json via the
|
|
713
782
|
// hardened settings-hook module (never writes through malformed JSON).
|
|
714
|
-
const
|
|
715
|
-
|
|
716
|
-
);
|
|
783
|
+
const settingsJsonPath = join(HOME, ".claude", "settings.json");
|
|
784
|
+
const { status: hookRemoveResult } = removeSessionStartHook(settingsJsonPath);
|
|
717
785
|
if (hookRemoveResult === "removed") {
|
|
718
786
|
console.log(
|
|
719
787
|
` ${GREEN}✔${RESET} Removed SessionStart hook from ${CYAN}~/.claude/settings.json${RESET}`,
|
|
@@ -727,6 +795,25 @@ async function uninstall() {
|
|
|
727
795
|
` ${YELLOW}⚠${RESET} Could not update ~/.claude/settings.json — remove the hook manually.`,
|
|
728
796
|
);
|
|
729
797
|
}
|
|
798
|
+
|
|
799
|
+
// Remove enforcement hooks (#1250): PreToolUse, SubagentStop interactive
|
|
800
|
+
// engine adapter, and SubagentStop annotation verifier.
|
|
801
|
+
const { status: preToolUseRemoveResult } = removePreToolUseHook(settingsJsonPath);
|
|
802
|
+
if (preToolUseRemoveResult === "removed") {
|
|
803
|
+
console.log(` ${GREEN}✔${RESET} Removed PreToolUse enforcement hook from ${CYAN}~/.claude/settings.json${RESET}`);
|
|
804
|
+
} else if (preToolUseRemoveResult !== "absent") {
|
|
805
|
+
console.log(` ${YELLOW}⚠${RESET} Could not remove PreToolUse hook — check ${CYAN}~/.claude/settings.json${RESET} manually.`);
|
|
806
|
+
}
|
|
807
|
+
const { status: subagentStopRemoveResult } = removeSubagentStopHook(settingsJsonPath);
|
|
808
|
+
if (subagentStopRemoveResult === "removed") {
|
|
809
|
+
console.log(` ${GREEN}✔${RESET} Removed SubagentStop hook from ${CYAN}~/.claude/settings.json${RESET}`);
|
|
810
|
+
}
|
|
811
|
+
const { status: subagentStopEnforceRemoveResult } = removeSubagentStopEnforceHook(settingsJsonPath);
|
|
812
|
+
if (subagentStopEnforceRemoveResult === "removed") {
|
|
813
|
+
console.log(` ${GREEN}✔${RESET} Removed SubagentStop enforcement hook from ${CYAN}~/.claude/settings.json${RESET}`);
|
|
814
|
+
} else if (subagentStopEnforceRemoveResult !== "absent") {
|
|
815
|
+
console.log(` ${YELLOW}⚠${RESET} Could not remove SubagentStop enforcement hook — check ${CYAN}~/.claude/settings.json${RESET} manually.`);
|
|
816
|
+
}
|
|
730
817
|
console.log("");
|
|
731
818
|
|
|
732
819
|
// Remove ForgeDock managed block from CLAUDE.md and AGENTS.md (if present).
|
|
@@ -832,6 +919,8 @@ async function update() {
|
|
|
832
919
|
console.log(`${BOLD}ForgeDock${RESET} — Checking for updates`);
|
|
833
920
|
console.log("");
|
|
834
921
|
|
|
922
|
+
console.log(` Mode: global (~/.claude)`);
|
|
923
|
+
|
|
835
924
|
// Check if installed via npm (no .git directory) or via git clone
|
|
836
925
|
const gitDir = join(FORGE_HOME, ".git");
|
|
837
926
|
if (existsSync(gitDir)) {
|
|
@@ -1035,6 +1124,10 @@ async function doctor() {
|
|
|
1035
1124
|
console.log(`${BOLD}ForgeDock Doctor${RESET} — Installation Health Check`);
|
|
1036
1125
|
console.log("");
|
|
1037
1126
|
|
|
1127
|
+
const { targetDir: TARGET_DIR } = detectInstallPaths();
|
|
1128
|
+
console.log(` Mode: global (~/.claude)`);
|
|
1129
|
+
console.log("");
|
|
1130
|
+
|
|
1038
1131
|
let failures = 0;
|
|
1039
1132
|
let warnings = 0;
|
|
1040
1133
|
|
|
@@ -1277,6 +1370,41 @@ async function doctor() {
|
|
|
1277
1370
|
}
|
|
1278
1371
|
}
|
|
1279
1372
|
|
|
1373
|
+
// ── Check 5b: 2026 feature-gated install capabilities ─────────────────────
|
|
1374
|
+
{
|
|
1375
|
+
const detectedVer = detectClaudeVersion();
|
|
1376
|
+
const breakpoints = loadBreakpoints(FORGE_HOME);
|
|
1377
|
+
if (detectedVer === null) {
|
|
1378
|
+
warn(
|
|
1379
|
+
"Version-gated features",
|
|
1380
|
+
"claude CLI not detected — cannot assess 2026 feature availability. Install or add claude to PATH.",
|
|
1381
|
+
);
|
|
1382
|
+
} else {
|
|
1383
|
+
const vStr = Array.isArray(detectedVer) ? detectedVer.join(".") : String(detectedVer);
|
|
1384
|
+
const featureChecks = [
|
|
1385
|
+
["effort levels", "effort_levels", "2.1.154"],
|
|
1386
|
+
["SubagentStop context injection", "stop_subagent_stop_additional_context", "2.1.163"],
|
|
1387
|
+
["Tool(param:value) permission rules", "tool_param_value_permission_rules", "2.1.178"],
|
|
1388
|
+
["skills/commands merge", "skills_commands_merge", "2.1.196"],
|
|
1389
|
+
["Sonnet 5 default + 1M context", "sonnet_5_default_1m_context", "2.1.197"],
|
|
1390
|
+
];
|
|
1391
|
+
const unavailable = featureChecks
|
|
1392
|
+
.filter(([, key]) => !hasFeature(breakpoints, key, vStr))
|
|
1393
|
+
.map(([label, , minVer]) => `${label} (requires v${minVer}+)`);
|
|
1394
|
+
if (unavailable.length === 0) {
|
|
1395
|
+
pass(
|
|
1396
|
+
"Version-gated features",
|
|
1397
|
+
`v${vStr} — all 2026 features available`,
|
|
1398
|
+
);
|
|
1399
|
+
} else {
|
|
1400
|
+
warn(
|
|
1401
|
+
"Version-gated features",
|
|
1402
|
+
`v${vStr} — unavailable: ${unavailable.join(", ")}. Update Claude Code to unlock.`,
|
|
1403
|
+
);
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1280
1408
|
// ── Check 6: CLAUDE.md legacy managed block (cwd, informational) ──────────
|
|
1281
1409
|
// The journey-based install never injects a CLAUDE.md block — session
|
|
1282
1410
|
// context comes from the SessionStart hook (see Check 5). A block's absence
|
|
@@ -1578,7 +1706,7 @@ function help() {
|
|
|
1578
1706
|
const commands = [
|
|
1579
1707
|
["Command", "Description"],
|
|
1580
1708
|
["npx forgedock", "Guided setup: install commands + configure repo (default)"],
|
|
1581
|
-
["npx forgedock install", "Install commands"],
|
|
1709
|
+
["npx forgedock install", "Install commands to ~/.claude/"],
|
|
1582
1710
|
["npx forgedock init", "Generate forge.yaml config for your project"],
|
|
1583
1711
|
["npx forgedock init --minimal", "Generate a minimal forge.yaml (required sections only)"],
|
|
1584
1712
|
["npx forgedock enable [dir]", "Activate ForgeDock in a directory"],
|
|
@@ -1586,8 +1714,11 @@ function help() {
|
|
|
1586
1714
|
["npx forgedock status [dir]", "Show ForgeDock state for a directory"],
|
|
1587
1715
|
["npx forgedock run <cmd> [args]", "Run a command headlessly via the Anthropic API"],
|
|
1588
1716
|
["npx forgedock run-issue <issue>", "Drive one issue through the durable engine"],
|
|
1717
|
+
["npx forgedock resume-stalled [--dry-run]", "Fleet stall recovery — re-dispatch expired-lease issues"],
|
|
1589
1718
|
["npx forgedock demo", "Set up a risk-free demo repo and print next steps"],
|
|
1590
1719
|
["npx forgedock labels [setup] [--repo owner/repo]", "Bootstrap ForgeDock-managed labels on a GitHub repo (idempotent)"],
|
|
1720
|
+
["npx forgedock watch [--repo owner/repo]", "Live per-agent orchestration view (Ctrl+C to exit)"],
|
|
1721
|
+
["npx forgedock report [--days 30] [--md] [--json]", "30-day pipeline impact receipts for your repo"],
|
|
1591
1722
|
["npx forgedock doctor", "Check installation health"],
|
|
1592
1723
|
["npx forgedock update", "Pull latest & reinstall"],
|
|
1593
1724
|
["npx forgedock uninstall", "Remove commands"],
|
|
@@ -1615,7 +1746,7 @@ function help() {
|
|
|
1615
1746
|
*
|
|
1616
1747
|
* Flags:
|
|
1617
1748
|
* --dry-run Preview the assembled prompt + tool plan; no API call.
|
|
1618
|
-
* --model <id> Override the model (default: claude-sonnet-
|
|
1749
|
+
* --model <id> Override the model (default: claude-sonnet-5 or $FORGEDOCK_MODEL).
|
|
1619
1750
|
* --max-iterations <n> Bound the tool-use loop (default: 50).
|
|
1620
1751
|
*
|
|
1621
1752
|
* The live loop requires ANTHROPIC_API_KEY and the optional @anthropic-ai/sdk
|
|
@@ -1713,13 +1844,215 @@ async function demo() {
|
|
|
1713
1844
|
}
|
|
1714
1845
|
}
|
|
1715
1846
|
|
|
1847
|
+
/**
|
|
1848
|
+
* `forgedock watch` — live per-agent orchestration view
|
|
1849
|
+
*
|
|
1850
|
+
* Polls GitHub every 5 seconds for in-flight issues (those with workflow:*
|
|
1851
|
+
* labels) and renders a per-agent table with issue#, title, phase, elapsed
|
|
1852
|
+
* time, and staleness status. All data is sourced from FORGE:HEARTBEAT
|
|
1853
|
+
* comments written by the pipeline at each phase boundary.
|
|
1854
|
+
*
|
|
1855
|
+
* Stalled agents (no HEARTBEAT update within pipeline.stall_timeout_minutes)
|
|
1856
|
+
* are highlighted in yellow.
|
|
1857
|
+
*
|
|
1858
|
+
* Run: npx forgedock watch [--repo owner/repo]
|
|
1859
|
+
* Exit: Ctrl+C
|
|
1860
|
+
*/
|
|
1861
|
+
async function watch() {
|
|
1862
|
+
const POLL_INTERVAL_MS = 5000;
|
|
1863
|
+
const USE_ANSI_WATCH = process.stdout.isTTY === true && !process.env.NO_COLOR && process.env.TERM !== "dumb";
|
|
1864
|
+
|
|
1865
|
+
// ── Resolve repo ──────────────────────────────────────────────────────────
|
|
1866
|
+
const watchRepo = resolveLabelsRepo(restArgs);
|
|
1867
|
+
if (!watchRepo) {
|
|
1868
|
+
process.stderr.write(
|
|
1869
|
+
`${RED}No repository found.${RESET}\n` +
|
|
1870
|
+
` Run from a directory with ${cyan("forge.yaml")}, or pass ${cyan("--repo owner/repo")}.\n`,
|
|
1871
|
+
);
|
|
1872
|
+
process.exitCode = 1;
|
|
1873
|
+
return;
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
// ── Validate gh CLI auth ───────────────────────────────────────────────────
|
|
1877
|
+
try {
|
|
1878
|
+
execSync("gh auth status", { stdio: ["ignore", "pipe", "ignore"] });
|
|
1879
|
+
} catch {
|
|
1880
|
+
process.stderr.write(
|
|
1881
|
+
`${RED}gh CLI is not authenticated.${RESET}\n` +
|
|
1882
|
+
` Fix: run ${cyan("gh auth login")} then retry.\n`,
|
|
1883
|
+
);
|
|
1884
|
+
process.exitCode = 1;
|
|
1885
|
+
return;
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
// ── Resolve stall timeout ─────────────────────────────────────────────────
|
|
1889
|
+
let stallMinutes = 15;
|
|
1890
|
+
const forgeYamlPath = join(process.cwd(), "forge.yaml");
|
|
1891
|
+
if (existsSync(forgeYamlPath)) {
|
|
1892
|
+
try {
|
|
1893
|
+
const raw = readFileSync(forgeYamlPath, "utf-8");
|
|
1894
|
+
const m = raw.match(/^\s*stall_timeout_minutes:\s*(\d+)/m);
|
|
1895
|
+
if (m) stallMinutes = parseInt(m[1], 10);
|
|
1896
|
+
} catch { /* ignore */ }
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
1900
|
+
function extractPhase(heartbeatBody) {
|
|
1901
|
+
const m = heartbeatBody.match(/\*\*Phase\*\*:\s*(.+)/);
|
|
1902
|
+
return m ? m[1].trim() : "unknown";
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
function extractTimestamp(heartbeatBody) {
|
|
1906
|
+
const m = heartbeatBody.match(/\*\*Timestamp\*\*:\s*(\S+)/);
|
|
1907
|
+
return m ? m[1].trim() : null;
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
function elapsedMin(isoTimestamp) {
|
|
1911
|
+
if (!isoTimestamp) return null;
|
|
1912
|
+
const ms = Date.now() - new Date(isoTimestamp).getTime();
|
|
1913
|
+
return Math.floor(ms / 60000);
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
function elapsedStr(minutes) {
|
|
1917
|
+
if (minutes === null) return "—";
|
|
1918
|
+
if (minutes < 60) return `${minutes}m`;
|
|
1919
|
+
return `${Math.floor(minutes / 60)}h ${minutes % 60}m`;
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
// ── Cleanup on exit ────────────────────────────────────────────────────────
|
|
1923
|
+
let intervalHandle;
|
|
1924
|
+
function cleanup() {
|
|
1925
|
+
if (intervalHandle) clearTimeout(intervalHandle);
|
|
1926
|
+
if (USE_ANSI_WATCH) {
|
|
1927
|
+
process.stdout.write("\x1b[?25h"); // show cursor
|
|
1928
|
+
}
|
|
1929
|
+
process.exit(0);
|
|
1930
|
+
}
|
|
1931
|
+
process.on("SIGINT", cleanup);
|
|
1932
|
+
process.on("SIGTERM", cleanup);
|
|
1933
|
+
|
|
1934
|
+
if (USE_ANSI_WATCH) {
|
|
1935
|
+
process.stdout.write("\x1b[?25l"); // hide cursor
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
// ── Render loop ────────────────────────────────────────────────────────────
|
|
1939
|
+
const WORKFLOW_LABELS = [
|
|
1940
|
+
"workflow:investigating",
|
|
1941
|
+
"workflow:ready-to-build",
|
|
1942
|
+
"workflow:building",
|
|
1943
|
+
"workflow:in-review",
|
|
1944
|
+
"needs-human",
|
|
1945
|
+
];
|
|
1946
|
+
|
|
1947
|
+
async function render() {
|
|
1948
|
+
const rows = [["#", "Title", "Phase", "Elapsed", "Status"]];
|
|
1949
|
+
let anyIssues = false;
|
|
1950
|
+
|
|
1951
|
+
for (const label of WORKFLOW_LABELS) {
|
|
1952
|
+
let issueJson;
|
|
1953
|
+
try {
|
|
1954
|
+
// Use execFileSync with argument array to avoid shell injection via watchRepo
|
|
1955
|
+
issueJson = execFileSync(
|
|
1956
|
+
"gh",
|
|
1957
|
+
["issue", "list", "-R", watchRepo, "--state", "open",
|
|
1958
|
+
"--label", label, "--limit", "30", "--json", "number,title"],
|
|
1959
|
+
{ encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] },
|
|
1960
|
+
);
|
|
1961
|
+
} catch {
|
|
1962
|
+
continue;
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
let issues;
|
|
1966
|
+
try { issues = JSON.parse(issueJson); } catch { continue; }
|
|
1967
|
+
|
|
1968
|
+
for (const issue of issues) {
|
|
1969
|
+
anyIssues = true;
|
|
1970
|
+
let phase = label.replace("workflow:", "");
|
|
1971
|
+
let elapsed = null;
|
|
1972
|
+
|
|
1973
|
+
// Fetch latest FORGE:HEARTBEAT comment for phase + timestamp
|
|
1974
|
+
try {
|
|
1975
|
+
const commentsJson = execFileSync(
|
|
1976
|
+
"gh",
|
|
1977
|
+
["api", `repos/${watchRepo}/issues/${issue.number}/comments`,
|
|
1978
|
+
"--jq", "[.[] | select(.body | contains(\"FORGE:HEARTBEAT\"))] | last | .body // \"\""],
|
|
1979
|
+
{ encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] },
|
|
1980
|
+
).trim();
|
|
1981
|
+
if (commentsJson && commentsJson !== '""' && commentsJson !== "") {
|
|
1982
|
+
const body = commentsJson.replace(/^"|"$/g, "").replace(/\\n/g, "\n");
|
|
1983
|
+
phase = extractPhase(body) || phase;
|
|
1984
|
+
const ts = extractTimestamp(body);
|
|
1985
|
+
elapsed = elapsedMin(ts);
|
|
1986
|
+
}
|
|
1987
|
+
} catch { /* use label-derived phase */ }
|
|
1988
|
+
|
|
1989
|
+
const isStalled = elapsed !== null && elapsed >= stallMinutes;
|
|
1990
|
+
const isBlocked = label === "needs-human";
|
|
1991
|
+
const status = isBlocked ? "BLOCKED" : isStalled ? "STALLED" : "running";
|
|
1992
|
+
|
|
1993
|
+
const titleTrunc = issue.title.length > 38 ? issue.title.slice(0, 35) + "..." : issue.title;
|
|
1994
|
+
const phaseShort = phase.length > 30 ? phase.slice(0, 27) + "..." : phase;
|
|
1995
|
+
|
|
1996
|
+
const statusColored = USE_ANSI_WATCH
|
|
1997
|
+
? (isBlocked ? `\x1b[31m${status}\x1b[0m` : isStalled ? `\x1b[33m${status}\x1b[0m` : `\x1b[32m${status}\x1b[0m`)
|
|
1998
|
+
: status;
|
|
1999
|
+
|
|
2000
|
+
const titleColored = USE_ANSI_WATCH && (isStalled || isBlocked)
|
|
2001
|
+
? `\x1b[33m${titleTrunc}\x1b[0m`
|
|
2002
|
+
: titleTrunc;
|
|
2003
|
+
|
|
2004
|
+
rows.push([
|
|
2005
|
+
`#${issue.number}`,
|
|
2006
|
+
titleColored,
|
|
2007
|
+
phaseShort,
|
|
2008
|
+
elapsedStr(elapsed),
|
|
2009
|
+
statusColored,
|
|
2010
|
+
]);
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
const now = new Date().toISOString().replace("T", " ").slice(0, 19) + " UTC";
|
|
2015
|
+
|
|
2016
|
+
if (USE_ANSI_WATCH) {
|
|
2017
|
+
process.stdout.write("\x1b[2J\x1b[H"); // clear screen + home
|
|
2018
|
+
} else {
|
|
2019
|
+
process.stdout.write(`\n${"─".repeat(60)}\n`);
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
process.stdout.write(`${BOLD}ForgeDock Watch${RESET} — ${dim(watchRepo)} ${dim(now)}\n\n`);
|
|
2023
|
+
|
|
2024
|
+
if (!anyIssues || rows.length === 1) {
|
|
2025
|
+
process.stdout.write(` ${dim("No in-flight issues. All quiet.")}\n`);
|
|
2026
|
+
} else {
|
|
2027
|
+
process.stdout.write(table(rows, { header: true }));
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
if (USE_ANSI_WATCH) {
|
|
2031
|
+
process.stdout.write(`\n${dim("Refreshing every 5s — Ctrl+C to exit")}\n`);
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
// Self-scheduling render loop — each render awaits completion before
|
|
2036
|
+
// scheduling the next, preventing overlapping cycles when GitHub API
|
|
2037
|
+
// latency exceeds the poll interval (forge#1428). The scheduled timeout is
|
|
2038
|
+
// the ONLY handle keeping the event loop alive for this command — it must
|
|
2039
|
+
// stay ref'd, or Node exits after the first render despite printing
|
|
2040
|
+
// "Refreshing every Ns — Ctrl+C to exit" (forge#1593). The only intended
|
|
2041
|
+
// exit path is SIGINT/SIGTERM → cleanup(), which restores the cursor.
|
|
2042
|
+
async function scheduleRender() {
|
|
2043
|
+
await render();
|
|
2044
|
+
intervalHandle = setTimeout(scheduleRender, POLL_INTERVAL_MS);
|
|
2045
|
+
}
|
|
2046
|
+
await scheduleRender();
|
|
2047
|
+
}
|
|
2048
|
+
|
|
1716
2049
|
// The journey-routed commands render their own branded marks (hero/compact);
|
|
1717
2050
|
// splash() would double-brand them. Engine-surface commands, help, and
|
|
1718
2051
|
// unknown-command output keep the logo.
|
|
1719
|
-
const SPLASH_COMMANDS = new Set(["run", "run-issue", "demo", "doctor", "help", "--help", "-h"]);
|
|
2052
|
+
const SPLASH_COMMANDS = new Set(["run", "run-issue", "resume-stalled", "demo", "doctor", "watch", "help", "--help", "-h"]);
|
|
1720
2053
|
const KNOWN_COMMANDS = new Set([
|
|
1721
2054
|
"install", "init", "enable", "disable", "status", "uninstall", "update",
|
|
1722
|
-
"run", "run-issue", "demo", "doctor", "help", "--help", "-h",
|
|
2055
|
+
"run", "run-issue", "resume-stalled", "demo", "doctor", "watch", "labels", "help", "--help", "-h",
|
|
1723
2056
|
]);
|
|
1724
2057
|
if (SPLASH_COMMANDS.has(command) || !KNOWN_COMMANDS.has(command)) splash();
|
|
1725
2058
|
|
|
@@ -1727,6 +2060,32 @@ let exitCode = 0;
|
|
|
1727
2060
|
switch (command) {
|
|
1728
2061
|
case "install": {
|
|
1729
2062
|
const c = ctx();
|
|
2063
|
+
// Version-gated install path (#1252): detect Claude Code version and advise
|
|
2064
|
+
// on features available or missing. Fail-open — null version skips checks.
|
|
2065
|
+
if (c.claudeVersion) {
|
|
2066
|
+
// Gate: effort levels require v2.1.154+
|
|
2067
|
+
if (!hasFeature(c.breakpoints, "effort_levels", c.claudeVersion)) {
|
|
2068
|
+
process.stderr.write(
|
|
2069
|
+
` ${YELLOW}note${RESET} Claude Code v${c.claudeVersion} detected — effort-level frontmatter requires v2.1.154+. ` +
|
|
2070
|
+
`Some orchestration features will use fallback mode.\n`
|
|
2071
|
+
);
|
|
2072
|
+
}
|
|
2073
|
+
// Gate: tool permission rules require v2.1.178+
|
|
2074
|
+
if (!hasFeature(c.breakpoints, "tool_param_value_permission_rules", c.claudeVersion)) {
|
|
2075
|
+
process.stderr.write(
|
|
2076
|
+
` ${YELLOW}note${RESET} Claude Code v${c.claudeVersion} detected — Tool(param:value) permission rules require v2.1.178+. ` +
|
|
2077
|
+
`Hook-based enforcement (#1250) will not be installed.\n`
|
|
2078
|
+
);
|
|
2079
|
+
} else {
|
|
2080
|
+
// v2.1.178+ supports permission rules — suggest opt-in rules for pipeline safety.
|
|
2081
|
+
process.stderr.write(
|
|
2082
|
+
` ${CYAN}tip${RESET} Claude Code v${c.claudeVersion} supports permission rules. ` +
|
|
2083
|
+
`Consider adding to ~/.claude/settings.json:\n` +
|
|
2084
|
+
` { "permissions": { "deny": ["Bash(git push origin main*)"] } }\n` +
|
|
2085
|
+
` This prevents accidental direct pushes to main.\n`
|
|
2086
|
+
);
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
1730
2089
|
if (existsSync(join(c.cwd, "forge.yaml")) && resolveState(c.cwd) === "managed-active") {
|
|
1731
2090
|
statusScreen(c);
|
|
1732
2091
|
} else {
|
|
@@ -1771,7 +2130,23 @@ switch (command) {
|
|
|
1771
2130
|
break;
|
|
1772
2131
|
case "run-issue": {
|
|
1773
2132
|
const { runFromCli } = await import("./engine-cli.mjs");
|
|
1774
|
-
|
|
2133
|
+
try {
|
|
2134
|
+
await runFromCli(restArgs);
|
|
2135
|
+
} catch (err) {
|
|
2136
|
+
process.stderr.write(`${RED}${err.message}${RESET}\n`);
|
|
2137
|
+
exitCode = 1;
|
|
2138
|
+
}
|
|
2139
|
+
break;
|
|
2140
|
+
}
|
|
2141
|
+
case "resume-stalled": {
|
|
2142
|
+
const { resumeStalledFromCli } = await import("./engine-cli.mjs");
|
|
2143
|
+
try {
|
|
2144
|
+
const result = await resumeStalledFromCli(restArgs);
|
|
2145
|
+
if (result && result.failed && result.failed.length > 0) exitCode = 1;
|
|
2146
|
+
} catch (err) {
|
|
2147
|
+
process.stderr.write(`${RED}${err.message}${RESET}\n`);
|
|
2148
|
+
exitCode = 1;
|
|
2149
|
+
}
|
|
1775
2150
|
break;
|
|
1776
2151
|
}
|
|
1777
2152
|
case "demo":
|
|
@@ -1780,6 +2155,14 @@ switch (command) {
|
|
|
1780
2155
|
case "doctor":
|
|
1781
2156
|
exitCode = await doctor();
|
|
1782
2157
|
break;
|
|
2158
|
+
case "report": {
|
|
2159
|
+
const { runReport } = await import("./report.mjs");
|
|
2160
|
+
await runReport(restArgs);
|
|
2161
|
+
break;
|
|
2162
|
+
}
|
|
2163
|
+
case "watch":
|
|
2164
|
+
await watch();
|
|
2165
|
+
break;
|
|
1783
2166
|
case "labels": {
|
|
1784
2167
|
const subcommand = positional[1];
|
|
1785
2168
|
if (!subcommand || subcommand === "setup" || subcommand.startsWith("--")) {
|