forgedock 1.0.9 → 1.0.10
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/bin/init-detect.mjs +1 -1
- package/bin/init-enrich-api.mjs +10 -2
- package/bin/tui.mjs +57 -43
- package/commands/work-on.md +18 -0
- package/docs/CONFIG.md +18 -0
- package/package.json +1 -1
package/bin/init-detect.mjs
CHANGED
package/bin/init-enrich-api.mjs
CHANGED
|
@@ -107,8 +107,16 @@ export function parseEnrichedDraft(output, draft) {
|
|
|
107
107
|
// Found the closing brace of the top-level object — parse the candidate.
|
|
108
108
|
try {
|
|
109
109
|
const enriched = JSON.parse(output.slice(start, i + 1));
|
|
110
|
-
// Basic sanity check: must have the required top-level sections
|
|
111
|
-
|
|
110
|
+
// Basic sanity check: must have the required top-level sections and
|
|
111
|
+
// each section must be a plain non-null object. The typeof+null guard
|
|
112
|
+
// rejects null and primitives; Array.isArray() additionally rejects
|
|
113
|
+
// arrays since typeof [] === 'object' would otherwise pass them.
|
|
114
|
+
if (
|
|
115
|
+
typeof enriched.project !== "object" || enriched.project === null || Array.isArray(enriched.project) ||
|
|
116
|
+
typeof enriched.paths !== "object" || enriched.paths === null || Array.isArray(enriched.paths) ||
|
|
117
|
+
typeof enriched.branches !== "object" || enriched.branches === null || Array.isArray(enriched.branches) ||
|
|
118
|
+
typeof enriched.meta !== "object" || enriched.meta === null || Array.isArray(enriched.meta)
|
|
119
|
+
) {
|
|
112
120
|
return draft;
|
|
113
121
|
}
|
|
114
122
|
return enriched;
|
package/bin/tui.mjs
CHANGED
|
@@ -106,9 +106,6 @@ export function box(content, { title = "", padding = 1, width } = {}) {
|
|
|
106
106
|
const lines = Array.isArray(content) ? content : String(content).split("\n");
|
|
107
107
|
const pad = " ".repeat(padding);
|
|
108
108
|
|
|
109
|
-
// Strip ANSI codes to compute visual width
|
|
110
|
-
const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*[A-Za-z]/g, "");
|
|
111
|
-
|
|
112
109
|
const contentWidth = lines.reduce(
|
|
113
110
|
(max, l) => Math.max(max, stripAnsi(l).length),
|
|
114
111
|
0,
|
|
@@ -659,8 +656,6 @@ export function table(
|
|
|
659
656
|
) {
|
|
660
657
|
if (!rows || rows.length === 0) return "";
|
|
661
658
|
|
|
662
|
-
const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*[A-Za-z]/g, "");
|
|
663
|
-
|
|
664
659
|
// Compute column widths
|
|
665
660
|
const colCount = Math.max(...rows.map((r) => r.length));
|
|
666
661
|
const widths = Array.from({ length: colCount }, (_, c) =>
|
|
@@ -701,6 +696,63 @@ export function table(
|
|
|
701
696
|
return lines.join("\n") + "\n";
|
|
702
697
|
}
|
|
703
698
|
|
|
699
|
+
// ---------------------------------------------------------------------------
|
|
700
|
+
// ANSI string utilities (exported for testing)
|
|
701
|
+
// ---------------------------------------------------------------------------
|
|
702
|
+
|
|
703
|
+
/** Strip all ANSI CSI sequences from a string, returning only visible text. */
|
|
704
|
+
export function stripAnsi(s) {
|
|
705
|
+
return s.replace(/\x1b\[[0-9;]*[A-Za-z]/g, "");
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Truncate an ANSI-decorated string to at most `maxWidth` visible characters,
|
|
710
|
+
* never bisecting an escape sequence.
|
|
711
|
+
*
|
|
712
|
+
* Tokens from the cut region are suppressed — only ANSI tokens encountered
|
|
713
|
+
* while the visible budget is still live are forwarded to the output.
|
|
714
|
+
* A trailing reset (\x1b[0m) is appended when truncation occurs and the
|
|
715
|
+
* result already contains ANSI sequences, preventing color bleed into
|
|
716
|
+
* adjacent columns.
|
|
717
|
+
*/
|
|
718
|
+
export function truncateVisible(str, maxWidth) {
|
|
719
|
+
const ansiRe = /\x1b\[[0-9;]*[A-Za-z]/g;
|
|
720
|
+
let visible = 0;
|
|
721
|
+
let result = "";
|
|
722
|
+
let lastIndex = 0;
|
|
723
|
+
let m;
|
|
724
|
+
ansiRe.lastIndex = 0;
|
|
725
|
+
while ((m = ansiRe.exec(str)) !== null) {
|
|
726
|
+
// Consume plain-text chars before this ANSI token
|
|
727
|
+
const plain = str.slice(lastIndex, m.index);
|
|
728
|
+
const remaining = maxWidth - visible;
|
|
729
|
+
if (remaining > 0) {
|
|
730
|
+
result += plain.slice(0, remaining);
|
|
731
|
+
visible += Math.min(plain.length, remaining);
|
|
732
|
+
}
|
|
733
|
+
// Only include the ANSI token while the visible budget is not yet exhausted.
|
|
734
|
+
// Once the cut point is reached, tokens from the removed region are suppressed;
|
|
735
|
+
// the post-loop reset guard below handles color-state cleanup.
|
|
736
|
+
if (visible < maxWidth) {
|
|
737
|
+
result += m[0];
|
|
738
|
+
}
|
|
739
|
+
lastIndex = ansiRe.lastIndex;
|
|
740
|
+
}
|
|
741
|
+
// Remaining plain text after last ANSI token
|
|
742
|
+
const remaining = maxWidth - visible;
|
|
743
|
+
if (remaining > 0) {
|
|
744
|
+
result += str.slice(lastIndex, lastIndex + remaining);
|
|
745
|
+
}
|
|
746
|
+
// If truncation occurred and the result contains any ANSI sequences,
|
|
747
|
+
// append a full reset to prevent color from bleeding into adjacent columns.
|
|
748
|
+
// Truncation is detected by checking whether the original string has more
|
|
749
|
+
// visible characters than maxWidth (i.e. stripAnsi(str).length > maxWidth).
|
|
750
|
+
if (stripAnsi(str).length > maxWidth && result.includes("\x1b[")) {
|
|
751
|
+
result += "\x1b[0m";
|
|
752
|
+
}
|
|
753
|
+
return result;
|
|
754
|
+
}
|
|
755
|
+
|
|
704
756
|
// ---------------------------------------------------------------------------
|
|
705
757
|
// Annotated Review Screen
|
|
706
758
|
// ---------------------------------------------------------------------------
|
|
@@ -821,50 +873,12 @@ export async function annotatedReviewScreen(
|
|
|
821
873
|
};
|
|
822
874
|
}
|
|
823
875
|
|
|
824
|
-
const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*[A-Za-z]/g, "");
|
|
825
|
-
|
|
826
876
|
// Pad an ANSI-decorated string to `width` visible characters.
|
|
827
877
|
function padVisible(str, width) {
|
|
828
878
|
const visual = stripAnsi(str).length;
|
|
829
879
|
return str + " ".repeat(Math.max(0, width - visual));
|
|
830
880
|
}
|
|
831
881
|
|
|
832
|
-
// Truncate an ANSI-decorated string to at most `maxWidth` visible characters,
|
|
833
|
-
// never bisecting an escape sequence.
|
|
834
|
-
function truncateVisible(str, maxWidth) {
|
|
835
|
-
const ansiRe = /\x1b\[[0-9;]*[A-Za-z]/g;
|
|
836
|
-
let visible = 0;
|
|
837
|
-
let result = "";
|
|
838
|
-
let lastIndex = 0;
|
|
839
|
-
let m;
|
|
840
|
-
ansiRe.lastIndex = 0;
|
|
841
|
-
while ((m = ansiRe.exec(str)) !== null) {
|
|
842
|
-
// Consume plain-text chars before this ANSI token
|
|
843
|
-
const plain = str.slice(lastIndex, m.index);
|
|
844
|
-
const remaining = maxWidth - visible;
|
|
845
|
-
if (remaining > 0) {
|
|
846
|
-
result += plain.slice(0, remaining);
|
|
847
|
-
visible += Math.min(plain.length, remaining);
|
|
848
|
-
}
|
|
849
|
-
// Always include the ANSI token (zero visible width)
|
|
850
|
-
result += m[0];
|
|
851
|
-
lastIndex = ansiRe.lastIndex;
|
|
852
|
-
}
|
|
853
|
-
// Remaining plain text after last ANSI token
|
|
854
|
-
const remaining = maxWidth - visible;
|
|
855
|
-
if (remaining > 0) {
|
|
856
|
-
result += str.slice(lastIndex, lastIndex + remaining);
|
|
857
|
-
}
|
|
858
|
-
// If truncation occurred and the result contains any ANSI sequences,
|
|
859
|
-
// append a full reset to prevent color from bleeding into adjacent columns.
|
|
860
|
-
// Truncation is detected by checking whether the original string has more
|
|
861
|
-
// visible characters than maxWidth (i.e. stripAnsi(str).length > maxWidth).
|
|
862
|
-
if (stripAnsi(str).length > maxWidth && result.includes("\x1b[")) {
|
|
863
|
-
result += "\x1b[0m";
|
|
864
|
-
}
|
|
865
|
-
return result;
|
|
866
|
-
}
|
|
867
|
-
|
|
868
882
|
// ── Render the annotated table ────────────────────────────────────────────
|
|
869
883
|
function renderScreen() {
|
|
870
884
|
process.stdout.write("\n");
|
package/commands/work-on.md
CHANGED
|
@@ -745,6 +745,8 @@ if iteration == max_iterations AND not PASS:
|
|
|
745
745
|
|
|
746
746
|
**After quality gate completes (PASS or fixes applied): proceed immediately to sub-phase 3H below. Quality gate is an intermediate check — "PASS" means the code is clean, NOT that the build is done. Do NOT stop.**
|
|
747
747
|
|
|
748
|
+
**After PASS: Do NOT re-read GitHub state, issue body, labels, or any file. Do NOT run any gh commands. Do NOT check PR status. Proceed directly to Phase 3H (Format and verify) below.** <!-- Added: forge#93 -->
|
|
749
|
+
|
|
748
750
|
### 3H: Format and verify
|
|
749
751
|
|
|
750
752
|
All tool commands are read from `forge.yaml → verification.commands`. When a key is absent, the step logs `SKIPPED — not configured in verification.commands` and continues rather than silently passing.
|
|
@@ -999,10 +1001,24 @@ PR #${PR_NUMBER} created targeting \`{PR_BASE}\`. Invoking /review-pr with --aut
|
|
|
999
1001
|
```
|
|
1000
1002
|
|
|
1001
1003
|
### 5C: Invoke /review-pr with --auto-merge
|
|
1004
|
+
|
|
1005
|
+
**Context budget check** (run before invoking review-pr): <!-- Added: forge#93 -->
|
|
1006
|
+
|
|
1007
|
+
Large-context sessions that accumulated significant build history cause review-pr to hit the token limit mid-review. Check the accumulated context before delegating:
|
|
1008
|
+
|
|
1009
|
+
- If the build changed **≥6 files** OR this agent has made **≥20 Skill invocations** since it started: invoke `work-on/review` as a fresh sub-agent (via `Skill(skill="work-on/review", args="...")`) rather than calling review-pr directly. The sub-agent starts with a clean context window.
|
|
1010
|
+
- Otherwise (small build, few skill calls): invoke review-pr directly as below.
|
|
1011
|
+
|
|
1012
|
+
**Direct invocation** (small builds — <6 changed files AND <20 Skill invocations):
|
|
1002
1013
|
```
|
|
1003
1014
|
Skill(skill="review-pr", args="{PR_NUMBER} --auto-merge --issue {NUMBER} --base {PR_BASE} --gh-flag {GH_FLAG}")
|
|
1004
1015
|
```
|
|
1005
1016
|
|
|
1017
|
+
**Sub-agent invocation** (large builds — ≥6 changed files OR ≥20 Skill invocations):
|
|
1018
|
+
```
|
|
1019
|
+
Skill(skill="work-on/review", args="{NUMBER} --repo {GH_REPO} --gh-flag {GH_FLAG} --worktree {WORKTREE_PATH} --branch {BRANCH} --base {PR_BASE}")
|
|
1020
|
+
```
|
|
1021
|
+
|
|
1006
1022
|
Review-pr handles: full domain-agent review → post findings as separate issues → merge PR. It does NOT close the issue or clean up the worktree — those run in Phase 6.
|
|
1007
1023
|
|
|
1008
1024
|
### 5D: Verify merge and close (recovery)
|
|
@@ -1020,6 +1036,8 @@ gh issue view {NUMBER} {GH_FLAG} --json state --jq '.state'
|
|
|
1020
1036
|
|
|
1021
1037
|
**After /review-pr returns and the PR is confirmed merged: immediately proceed to Phase 6 (Close & Cleanup). Do NOT stop here. `REVIEW_RESULT: status: COMPLETE` is an intermediate result — the pipeline is NOT done. Invoke Phase 6 now to close the issue, update labels, post the trajectory log, and clean up the worktree.**
|
|
1022
1038
|
|
|
1039
|
+
**Do NOT output any text describing this transition. Do NOT write phrases like "returning to work-on", "proceeding to close", "now invoking Phase 6", or any narrative summary of what comes next. Do NOT emit end_turn. Execute Phase 6 code immediately.** <!-- Added: forge#93 -->
|
|
1040
|
+
|
|
1023
1041
|
---
|
|
1024
1042
|
|
|
1025
1043
|
## Phase 6: Close & Cleanup
|
package/docs/CONFIG.md
CHANGED
|
@@ -665,6 +665,24 @@ The `registry` module resolves one of three states for any directory:
|
|
|
665
665
|
|
|
666
666
|
A missing or corrupt `registry.json` is treated as an empty opt-out set. The registry always fails open — it never throws and never blocks a Claude Code session.
|
|
667
667
|
|
|
668
|
+
### Downgrade Behaviour
|
|
669
|
+
|
|
670
|
+
Registry keys are the **real path** of a directory (resolved via `realpathSync`) rather than the raw `resolve()` path. This has been the case since v1.0.x (PR #467, which fixed symlinked-directory key mismatches).
|
|
671
|
+
|
|
672
|
+
If you downgrade to a build older than PR #467 **and** your project is accessed via a symlinked directory path, the older build looks up registry entries using the pre-symlink `resolve()`-only key form. It will not find entries written by the newer build's real-path keys. The practical effect is benign:
|
|
673
|
+
|
|
674
|
+
- Opt-outs set on the newer build briefly stop applying for one session.
|
|
675
|
+
- The one-time "Enable ForgeDock here?" nudge may reappear once.
|
|
676
|
+
- No data is lost. No crash. Fail-open behaviour holds throughout.
|
|
677
|
+
|
|
678
|
+
**Recovery**: after downgrading, re-apply your opt-out with the older build:
|
|
679
|
+
|
|
680
|
+
```bash
|
|
681
|
+
npx forgedock disable [dir]
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
This re-writes the entry under the key form the older build expects.
|
|
685
|
+
|
|
668
686
|
### Managing Opt-Out State
|
|
669
687
|
|
|
670
688
|
Use the `forgedock enable` and `forgedock disable` commands to add or remove a directory from the opt-out set:
|