forgedock 1.7.0 → 1.8.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 +2 -1
- package/bin/engine/admission.mjs +242 -7
- package/bin/engine/admission.test.mjs +183 -2
- package/bin/engine/phases.mjs +31 -22
- package/bin/engine/resolve.mjs +8 -1
- package/bin/engine/resolve.test.mjs +5 -0
- package/bin/engine-cli.mjs +2 -0
- package/bin/engine.mjs +26 -17
- package/bin/forgedock.mjs +124 -48
- package/bin/hooks/pre-tool-use.mjs +573 -2
- package/bin/journey.mjs +11 -7
- package/bin/labels.json +5 -0
- package/bin/opencode-adapter.mjs +652 -92
- package/bin/orchestrate-preflight.mjs +434 -0
- package/bin/runner.mjs +77 -9
- package/commands/audit-agents.md +75 -26
- package/commands/autopilot.md +4 -4
- package/commands/cleanup.md +10 -5
- package/commands/issue.md +95 -35
- package/commands/orchestrate/phase-1-resolve.md +70 -32
- package/commands/orchestrate/phase-3-dependency.md +167 -22
- package/commands/orchestrate/phase-4-execution.md +691 -62
- package/commands/orchestrate/phase-5-cleanup.md +5 -11
- package/commands/orchestrate/phase-6-report.md +27 -1
- package/commands/orchestrate/safety.md +1 -1
- package/commands/orchestrate.md +39 -1
- package/commands/quality-gate.md +30 -10
- package/commands/review-pr-agents/protocols.md +13 -0
- package/commands/review-pr-agents.md +8 -0
- package/commands/review-pr-staging.md +117 -17
- package/commands/review-pr.md +95 -23
- package/commands/work-on/build/validate.md +25 -4
- package/commands/work-on/close.md +40 -10
- package/commands/work-on/decompose.md +33 -1
- package/commands/work-on/investigate.md +33 -0
- package/commands/work-on/remediate.md +40 -5
- package/commands/work-on/review.md +27 -0
- package/commands/work-on.md +48 -5
- package/docs/CONFIG.md +51 -15
- package/docs/OPENCODE.md +128 -25
- package/docs/spec/review-protocol.md +7 -0
- package/package.json +1 -1
- package/scripts/check-admission-parity.mjs +1 -0
- package/scripts/check-command-side-effects.sh +14 -13
- package/scripts/check-shell-portability.sh +36 -0
- package/scripts/check-spec-markers.sh +21 -11
- package/scripts/checkbox-sections.test.sh +67 -0
- package/scripts/extract-affected-files.sh +85 -5
- package/scripts/extract-affected-files.test.sh +154 -2
package/commands/work-on.md
CHANGED
|
@@ -273,7 +273,7 @@ If detected, dispatch immediately and STOP — do NOT fall through to Phase 0B's
|
|
|
273
273
|
Skill(skill="work-on/remediate", args="${REMEDIATE_PR_NUMBER} ${REMEDIATE_ISSUE_FLAG} --repo {GH_REPO} --gh-flag {GH_FLAG}")
|
|
274
274
|
```
|
|
275
275
|
|
|
276
|
-
**After `REMEDIATE_RESULT` returns, STOP unconditionally** — do not run any further Phase 0–7 logic in this file. `work-on/remediate.md` is self-contained:
|
|
276
|
+
**After `REMEDIATE_RESULT` returns, STOP unconditionally** — do not run any further Phase 0–7 logic in this file. `work-on/remediate.md` is self-contained: a FIXABLE remediation replaces `needs-human` with the active `workflow:in-review` state only while it is running, then ends at `workflow:merged`, `workflow:awaiting-merge`, or a newly asserted `needs-human` label. When `re_gate_outcome: AUTO-LANDED`, it drives its own close phase internally (Phase M8 invokes `Skill("work-on:close", ...)` directly) before returning. For every other outcome (`HELD-AWAITING-MERGE`, `RE-ESCALATED`, `UNFIXABLE`, `BLOCKED`, `ALREADY_DONE`), the issue is already at a terminal state (`workflow:awaiting-merge` or `needs-human`, or already closed) per the Universal Phase Dispatcher — nothing further to do.
|
|
277
277
|
|
|
278
278
|
This mode is reachable both standalone (a human or script running `/work-on <pr> --remediate` directly) and via the orchestrator (`commands/orchestrate/phase-4-execution.md` item 6.4 auto-dispatches the identical `Skill(skill='work-on', args='{PR} --remediate --issue {N} ...')` invocation against a `needs-human`-gated predecessor's own PR).
|
|
279
279
|
|
|
@@ -358,9 +358,21 @@ fi
|
|
|
358
358
|
|
|
359
359
|
**Batch issue pipeline rules** (when `IS_BATCH=true`):
|
|
360
360
|
- Build phases execute exactly as normal (the batch issue body IS the spec for what to fix)
|
|
361
|
-
-
|
|
361
|
+
- Batch members are referenced in the PR body with `Refs #N`, never `Closes #N`; the batch issue is the only issue the PR may close.
|
|
362
|
+
- After successful merge, re-read every member's live state and labels before closing it. A member that is `needs-human`, `blocked`, or `operator-only` remains open and is reported as a split outcome:
|
|
362
363
|
```bash
|
|
363
364
|
for MEMBER in "${BATCH_MEMBERS[@]}"; do
|
|
365
|
+
MEMBER_SNAPSHOT=$(gh issue view "$MEMBER" {GH_FLAG} --json state,labels \
|
|
366
|
+
--jq '{state: .state, labels: [.labels[].name]}' 2>/dev/null) || {
|
|
367
|
+
echo "WARNING: could not verify batch member #${MEMBER}; leaving it open"
|
|
368
|
+
continue
|
|
369
|
+
}
|
|
370
|
+
MEMBER_GATED=$(echo "$MEMBER_SNAPSHOT" | jq -r \
|
|
371
|
+
'(.state != "OPEN") or ([.labels[] | select(. == "needs-human" or . == "blocked" or . == "operator-only")] | length > 0)')
|
|
372
|
+
if [ "$MEMBER_GATED" = "true" ]; then
|
|
373
|
+
echo "SPLIT OUTCOME: #${MEMBER} remains open because it requires a human or operator action."
|
|
374
|
+
continue
|
|
375
|
+
fi
|
|
364
376
|
gh issue close "$MEMBER" {GH_FLAG} \
|
|
365
377
|
--comment "Resolved as part of batch PR #{PR_NUMBER} (#{ISSUE_NUMBER}). See batch issue for details."
|
|
366
378
|
gh issue edit "$MEMBER" {GH_FLAG} --add-label "workflow:merged" 2>/dev/null || true
|
|
@@ -2081,11 +2093,42 @@ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:CHECKPOINT -->
|
|
|
2081
2093
|
|
|
2082
2094
|
```bash
|
|
2083
2095
|
BODY=$(gh issue view {NUMBER} {GH_FLAG} --json body --jq '.body')
|
|
2084
|
-
REMAINING_BEFORE=$(
|
|
2085
|
-
|
|
2096
|
+
REMAINING_BEFORE=$(printf '%s\n' "$BODY" | grep -cE '^[-*+] \[ \]' || true)
|
|
2097
|
+
|
|
2098
|
+
# Structural test: count heading-delimited sections that contain checkbox items.
|
|
2099
|
+
# Multi-phase == 2+ checkbox-bearing sections. A single checkbox group is
|
|
2100
|
+
# single-phase regardless of how many prose headings surround it.
|
|
2101
|
+
#
|
|
2102
|
+
# - Fenced code blocks are stripped first: issue bodies routinely embed fenced
|
|
2103
|
+
# blocks whose lines start with '#' or contain a literal '- [ ]'. An
|
|
2104
|
+
# unterminated fence keeps the original body so later work is never hidden.
|
|
2105
|
+
# - ATX headings and setext underlines both delimit sections. The ATX pattern
|
|
2106
|
+
# avoids awk interval-quantifier variance across awk implementations.
|
|
2107
|
+
# - grep -E / awk only — no PCRE. '^#+ ' needs none.
|
|
2108
|
+
FENCE_COUNT=$(printf '%s\n' "$BODY" | grep -cE '^(```+|~~~+)' || true)
|
|
2109
|
+
if [ $(( ${FENCE_COUNT:-0} % 2 )) -ne 0 ]; then
|
|
2110
|
+
BODY_STRIPPED="$BODY"
|
|
2111
|
+
else
|
|
2112
|
+
BODY_STRIPPED=$(printf '%s\n' "$BODY" | awk '/^(```+|~~~+)/{f=!f; next} !f')
|
|
2113
|
+
fi
|
|
2114
|
+
|
|
2115
|
+
CHECKBOX_SECTIONS=$(printf '%s\n' "$BODY_STRIPPED" | awk '
|
|
2116
|
+
/^#+ / { if (in_section && has) n++; in_section=1; has=0; previous=""; next }
|
|
2117
|
+
/^(=+|-+)$/ && previous != "" { if (in_section && has) n++; in_section=1; has=0; previous=""; next }
|
|
2118
|
+
{ if (in_section && /^[-*+] \[[ xX]\]/) has=1; previous=$0 }
|
|
2119
|
+
END { if (in_section && has) n++; print n+0 }
|
|
2120
|
+
')
|
|
2121
|
+
|
|
2122
|
+
# Sub-issue-tracker guard: a decompose parent whose only checkbox group is
|
|
2123
|
+
# '## Sub-Issue Tracker' counts 1 section. Checking those off would mark open
|
|
2124
|
+
# sub-issues done and close the tracker, so any unchecked GFM task item for an
|
|
2125
|
+
# issue forces multi-phase.
|
|
2126
|
+
SUBISSUE_ITEMS=$(printf '%s\n' "$BODY_STRIPPED" | grep -cE '^[-*+] \[ \] #[0-9]+' || true)
|
|
2086
2127
|
```
|
|
2087
2128
|
|
|
2088
|
-
|
|
2129
|
+
**Sync invariant:** Keep the structural computation above and its consuming multi-phase guard in `commands/work-on/close.md` Phase C1 synchronized. The guard is `CHECKBOX_SECTIONS >= 2 OR SUBISSUE_ITEMS > 0`; this Phase 6A path is reached only when `REMAINING_BEFORE > 0`. <!-- Fixed: forge#2840, #2874 -->
|
|
2130
|
+
|
|
2131
|
+
If multi-phase (`CHECKBOX_SECTIONS >= 2` OR a `- [ ] #NNN` sub-issue item is present): do NOT check off future phase items. Add PR reference only. A heading count is **not** the test — every templated issue carries `## Problem`/`## Evidence`/`## Context`, so it is `> 0` universally.
|
|
2089
2132
|
|
|
2090
2133
|
If single-phase or final phase: check off all `[ ]` items, add PR reference.
|
|
2091
2134
|
|
package/docs/CONFIG.md
CHANGED
|
@@ -298,9 +298,13 @@ Concurrency limits and cascade admission policy for `/orchestrate`'s batch engin
|
|
|
298
298
|
|
|
299
299
|
```yaml
|
|
300
300
|
orchestration:
|
|
301
|
-
# Maximum number of /work-on
|
|
302
|
-
#
|
|
303
|
-
#
|
|
301
|
+
# Maximum number of top-level /work-on agents dispatched concurrently. Each
|
|
302
|
+
# worker normally fans out to about 8 total subagent spawns (build, quality
|
|
303
|
+
# gate, and review included), so set this to no more than session_budget / 8.
|
|
304
|
+
# Default: 12; use 25 or fewer for a 200-spawn session budget. This limits
|
|
305
|
+
# in-flight workers only: under cascade.policy: all, raising it increases
|
|
306
|
+
# both completion and finding-spawn throughput and does not itself bound or
|
|
307
|
+
# shrink cascade queue growth.
|
|
304
308
|
max_concurrent: 12
|
|
305
309
|
|
|
306
310
|
cascade:
|
|
@@ -324,13 +328,26 @@ orchestration:
|
|
|
324
328
|
# spawned from a review-finding. Accepts a positive integer or the literal
|
|
325
329
|
# string "unlimited". Governs Phase 1 resolve-time admission only (the
|
|
326
330
|
# `cascade`/`review-findings`/`findings` resolution in phase-1-resolve.md)
|
|
327
|
-
# — it does NOT
|
|
328
|
-
#
|
|
329
|
-
#
|
|
330
|
-
#
|
|
331
|
+
# — it does NOT individually dispatch deferred generation-2+ findings in
|
|
332
|
+
# Step 4C. Bounded P3 batching is the sole automated exception: it may
|
|
333
|
+
# aggregate a finite generation-2 member set under batch_max_generation,
|
|
334
|
+
# as implemented by #2849. This is what makes "admit gen-2, stop at gen-3"
|
|
335
|
+
# expressible, which the pre-#2234 --allow-gen2 flag could not say.
|
|
331
336
|
# Default: 1 (from the "balanced" preset); "unlimited" under "all".
|
|
332
337
|
max_generation: 1
|
|
333
338
|
|
|
339
|
+
# Maximum generation a P3 batch may aggregate. Batching is a sanctioned,
|
|
340
|
+
# auditable exception to Step 4C's autonomous cascade cap: it can fold
|
|
341
|
+
# several deferred low-priority findings into one reviewed unit, but it
|
|
342
|
+
# must never make recursion unbounded. Every batch records the maximum
|
|
343
|
+
# member generation in FORGE:BATCH_MAX_GENERATION. This value is always
|
|
344
|
+
# finite, even under policy: all. Default: 2.
|
|
345
|
+
batch_max_generation: 2
|
|
346
|
+
|
|
347
|
+
# Permit closure only of mechanically identical bot/app alerts: same
|
|
348
|
+
# normalized title, generator, and trigger condition, while retaining one
|
|
349
|
+
# canonical issue. Human reports are never included. Default: false.
|
|
350
|
+
dedup_automated: false
|
|
334
351
|
# Per-batch token ceiling for Step 4C's review-finding cascade dispatch.
|
|
335
352
|
# New home for the same lever previously read only as
|
|
336
353
|
# `pipeline.token_budget_per_batch` (kept working as a deprecated alias —
|
|
@@ -353,17 +370,30 @@ orchestration:
|
|
|
353
370
|
# (avoids serializing agents on same-file predecessor edges). Default:
|
|
354
371
|
# true (from "balanced"); false under "all".
|
|
355
372
|
p3_same_file_defer: true
|
|
373
|
+
|
|
374
|
+
# Running findings-spawned / merged-units ceiling. "off" (the default)
|
|
375
|
+
# only reports amplification; a positive decimal defers further
|
|
376
|
+
# same-lineage refinements to the completion sweep once exceeded.
|
|
377
|
+
max_amplification: "off"
|
|
378
|
+
|
|
379
|
+
# Number of consecutive merged-unit observations at ratio >= 1.0 before
|
|
380
|
+
# printing a convergence warning. Default: 3.
|
|
381
|
+
convergence_window: 3
|
|
356
382
|
```
|
|
357
383
|
|
|
358
384
|
| Field | Type | Required | Description |
|
|
359
385
|
|-------|------|----------|-------------|
|
|
360
|
-
| `max_concurrent` | integer | No | Max concurrent `/work-on`
|
|
386
|
+
| `max_concurrent` | integer | No | Max concurrent top-level `/work-on` dispatches. It does not bound cascade growth; under `policy: all` it raises drain and spawn throughput together. Default: 12. |
|
|
361
387
|
| `cascade.policy` | string | No | `all` \| `balanced` \| `conservative`. Default: `balanced` |
|
|
362
388
|
| `cascade.max_generation` | integer or `"unlimited"` | No | Max cascade generation admitted at Phase 1 resolve time. Default: 1 |
|
|
389
|
+
| `cascade.batch_max_generation` | integer | No | Finite maximum member generation that an automated P3 batch may aggregate. Default: 2 |
|
|
390
|
+
| `cascade.dedup_automated` | boolean | No | Allow only mechanically identical bot/app alerts to be closed with a retained canonical issue. Default: `false` |
|
|
363
391
|
| `cascade.token_budget` | integer or `"unlimited"` | No | Per-batch token ceiling for Step 4C cascade dispatch. Default: 900000 (deprecated alias: `pipeline.token_budget_per_batch`) |
|
|
364
392
|
| `cascade.defer_on_batch_gated` | boolean | No | Suppress cascade dispatch when the original batch is fully human-gated. Default: `true` |
|
|
365
393
|
| `cascade.keyword_heuristic` | boolean | No | Defer P3-and-below comment/typo-titled findings. Default: `true` |
|
|
366
394
|
| `cascade.p3_same_file_defer` | boolean | No | Defer P3 findings sharing a file with the active batch. Default: `true` |
|
|
395
|
+
| `cascade.max_amplification` | positive number or `"off"` | No | Opt-in ceiling for same-lineage refinement dispatch. Default: `"off"` |
|
|
396
|
+
| `cascade.convergence_window` | positive integer | No | Consecutive ratio observations at or above 1.0 before warning. Default: 3 |
|
|
367
397
|
|
|
368
398
|
**Hard invariant — not configurable**: safety exclusions (findings whose `## Problem` section indicates security/billing/anti-bot/auth concerns) are never batched and never auto-admitted by any `cascade.policy`, including `all`. That exclusion is enforced upstream of this section (the P3 batching eligibility check) and has no corresponding key here by design.
|
|
369
399
|
|
|
@@ -375,10 +405,14 @@ orchestration:
|
|
|
375
405
|
|
|
376
406
|
## `pipeline` (OPTIONAL)
|
|
377
407
|
|
|
378
|
-
Tuning knobs for `/orchestrate`'s batch engine — stall
|
|
408
|
+
Tuning knobs for the CLI backend and `/orchestrate`'s batch engine — invocation and stall timeouts, narration verbosity, and (deprecated alias only, see `orchestration.cascade` above) the review-finding cascade token budget. All keys optional; sane defaults apply when omitted.
|
|
379
409
|
|
|
380
410
|
```yaml
|
|
381
411
|
pipeline:
|
|
412
|
+
# Wall-clock limit for one CLI backend invocation. The environment variable
|
|
413
|
+
# FORGEDOCK_CLI_TIMEOUT_MS (milliseconds) takes precedence. Default: 60.
|
|
414
|
+
cli_timeout_minutes: 60
|
|
415
|
+
|
|
382
416
|
# Minutes an agent may sit idle (no FORGE:HEARTBEAT / label change) before
|
|
383
417
|
# /orchestrate's stall detector treats it as stuck and attempts an
|
|
384
418
|
# auto-resume. Default: 15.
|
|
@@ -401,12 +435,13 @@ pipeline:
|
|
|
401
435
|
|
|
402
436
|
| Field | Type | Required | Description |
|
|
403
437
|
|-------|------|----------|-------------|
|
|
438
|
+
| `cli_timeout_minutes` | integer | No | Wall-clock minutes for one CLI backend invocation. Default: 60. `FORGEDOCK_CLI_TIMEOUT_MS` (milliseconds) takes precedence. |
|
|
404
439
|
| `stall_timeout_minutes` | integer | No | Idle minutes before the stall detector attempts auto-resume. Default: 15 |
|
|
405
440
|
| `token_budget_per_batch` | integer | No | **Deprecated** — use `orchestration.cascade.token_budget`. Default: 900000 |
|
|
406
441
|
| `token_estimate_per_finding` | integer | No | Estimated tokens per P3-or-lower cascade finding. Default: 150000 |
|
|
407
442
|
| `narration` | string | No | `terse` \| `verbose`. Default: `terse` |
|
|
408
443
|
|
|
409
|
-
**Commands that use this section**: `orchestrate`
|
|
444
|
+
**Commands that use this section**: CLI backend (`cli_timeout_minutes`); `orchestrate` (remaining keys)
|
|
410
445
|
|
|
411
446
|
---
|
|
412
447
|
|
|
@@ -927,7 +962,7 @@ The hook entry is registered idempotently: re-installing (`npx forgedock update`
|
|
|
927
962
|
| Command | Action |
|
|
928
963
|
|---------|--------|
|
|
929
964
|
| `npx forgedock` / `npx forgedock install` | Registers the SessionStart hook (along with installing commands and writing `forge.yaml`) |
|
|
930
|
-
| `npx forgedock update` | Re-registers the hook (relink only — does not touch `forge.yaml`) |
|
|
965
|
+
| `npx forgedock update` | Re-registers the hook when the resolved source can be safely refreshed (relink only — does not touch `forge.yaml`) |
|
|
931
966
|
| `npx forgedock uninstall` | Removes the hook entry from `settings.json` |
|
|
932
967
|
|
|
933
968
|
### Opting Out
|
|
@@ -948,7 +983,7 @@ See [Per-Directory State Registry](#per-directory-state-registry) below for how
|
|
|
948
983
|
|
|
949
984
|
`npx`/`npm` never installs `forgedock` to a stable location on your machine — it resolves to wherever `npx`'s cache (or a global npm install, or a git clone) happens to physically live. Historically, `~/.claude/commands/` and the SessionStart hook's script path were symlinked **directly** from that ephemeral location. When npm/npx (or the OS) later evicted its cache, those symlinks silently dangled — Claude Code sessions stopped getting ForgeDock context with no error shown.
|
|
950
985
|
|
|
951
|
-
|
|
986
|
+
For npm/npx sources, every `npx forgedock` (or `npx forgedock update`) run copies ForgeDock's own installable payload into a stable, version-independent home:
|
|
952
987
|
|
|
953
988
|
```
|
|
954
989
|
~/.forge/
|
|
@@ -968,8 +1003,8 @@ As of this feature, every `npx forgedock` (or `npx forgedock update`) run copies
|
|
|
968
1003
|
| `npx forgedock` / `npx forgedock install` | Copies the resolved package's payload into `~/.forge/` (content-compared — unchanged files are never rewritten) before linking commands, then links from `~/.forge/`, not the original source |
|
|
969
1004
|
| `npx forgedock init` | Does not touch `~/.forge/` — `init` only writes `forge.yaml`, it never installs commands |
|
|
970
1005
|
| `npx forgedock update` (npm/npx install) | Refreshes `~/.forge/` from whatever the currently-resolved package looks like, then relinks |
|
|
971
|
-
| `npx forgedock update` (git-clone install) |
|
|
972
|
-
| `npx forgedock doctor` | Reports `~/.forge/version` and confirms `~/.forge/{bin,commands,scripts,templates}`
|
|
1006
|
+
| `npx forgedock update` (git-clone install) | Updates and relinks a `main` checkout; a non-main checkout is left untouched and gets actionable guidance because the current source must remain the source used after the command returns |
|
|
1007
|
+
| `npx forgedock doctor` | Reports the effective source path, version, and git branch, then reports `~/.forge/version` and confirms `~/.forge/{bin,commands,scripts,templates}` for npm installs |
|
|
973
1008
|
|
|
974
1009
|
The copy is idempotent: files whose content is already byte-identical are never rewritten, so steady-state re-runs are fast and don't generate false "updated" noise.
|
|
975
1010
|
|
|
@@ -979,6 +1014,7 @@ If you run ForgeDock from a git clone (or a git worktree) of the repo itself —
|
|
|
979
1014
|
|
|
980
1015
|
- `~/.claude/commands/` continues to symlink directly from the clone, exactly as before this feature.
|
|
981
1016
|
- `~/.forge/{bin,commands,scripts,templates}` correctly does **not** exist for this install mode — `npx forgedock doctor` treats that absence as healthy, not a warning.
|
|
1017
|
+
- `npx forgedock update` never switches a non-main checkout to `main` and then restores it. That transient update would leave the next invocation running the old branch source, so the command reports the effective path, branch, and version and tells you to run from a main checkout instead.
|
|
982
1018
|
|
|
983
1019
|
### Not the Same as `~/.forge/{runs,index}`
|
|
984
1020
|
|
|
@@ -1086,7 +1122,7 @@ The directory (`~/.forge/`) is created automatically on first write with `mkdir(
|
|
|
1086
1122
|
### When It's Written
|
|
1087
1123
|
|
|
1088
1124
|
- After every successful `npx forgedock install` (end of the onboarding journey, right after the "Forged." celebration screen).
|
|
1089
|
-
- After every `npx forgedock update` —
|
|
1125
|
+
- After every successful `npx forgedock update` — the main git-clone branch and npm branch route through the same repair step (`relinkAndHint()`), so the receipt is refreshed either way. A non-main git-clone advisory intentionally does not relink or rewrite the receipt.
|
|
1090
1126
|
|
|
1091
1127
|
The write is best-effort and never blocks or fails the install/update it records: any error (permission denied, disk full, unusual `FORGE_HOME` layout) degrades silently to a no-op, matching the fail-open contract already used by the command-manifest and hook-registration steps.
|
|
1092
1128
|
|
package/docs/OPENCODE.md
CHANGED
|
@@ -19,12 +19,12 @@ For the optional command tier:
|
|
|
19
19
|
npx forgedock opencode install --extras
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
Restart OpenCode after install or update. OpenCode loads commands and
|
|
23
|
-
at startup.
|
|
22
|
+
Restart OpenCode after install or update. OpenCode loads commands, skills, and
|
|
23
|
+
plugins at startup.
|
|
24
24
|
|
|
25
|
-
The
|
|
26
|
-
ForgeDock-owned files under OpenCode's config directory and records
|
|
27
|
-
`forgedock/manifest.json` for safe updates and removal.
|
|
25
|
+
The native adapter does not add or modify user-owned OpenCode settings. It
|
|
26
|
+
writes only ForgeDock-owned files under OpenCode's config directory and records
|
|
27
|
+
them in `forgedock/manifest.json` for safe updates and removal.
|
|
28
28
|
|
|
29
29
|
## Usage
|
|
30
30
|
|
|
@@ -62,9 +62,29 @@ The generated plugin has no prompt text. It:
|
|
|
62
62
|
- injects `FORGE_HOME` into OpenCode shell environments;
|
|
63
63
|
- defaults `subagent_depth` to 2 when the user has not configured it, while
|
|
64
64
|
preserving explicit lower limits;
|
|
65
|
+
- grants the built-in `general` subagent permission to invoke native `task`
|
|
66
|
+
unless the user explicitly configured a task permission;
|
|
67
|
+
- opts into OpenCode background subagents for the orchestrator's explicit
|
|
68
|
+
ready-issue dispatches so each completed issue can wake the parent independently; set
|
|
69
|
+
`OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=false` to opt out;
|
|
65
70
|
- selects Git Bash on Windows only when the user has not explicitly configured
|
|
66
71
|
an OpenCode shell, because the shared workflows and helper scripts use Bash.
|
|
67
72
|
|
|
73
|
+
The adapter also registers every eligible ForgeDock workflow as a native
|
|
74
|
+
OpenCode skill. Top-level and nested source paths are normalized to valid
|
|
75
|
+
hyphenated skill names while the wrapper continues to point at the authoritative
|
|
76
|
+
source file:
|
|
77
|
+
|
|
78
|
+
```text
|
|
79
|
+
commands/work-on.md -> skills/work-on/SKILL.md
|
|
80
|
+
commands/work-on/investigate.md -> skills/work-on-investigate/SKILL.md
|
|
81
|
+
commands/review-pr.md -> skills/review-pr/SKILL.md
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The wrapper is intentionally thin. It carries the workflow's `name` and
|
|
85
|
+
`description` frontmatter, preserves the current-context arguments, and loads
|
|
86
|
+
only the referenced `commands/**/*.md` spec.
|
|
87
|
+
|
|
68
88
|
OpenCode's provider configuration remains entirely user-owned. ForgeDock does
|
|
69
89
|
not require Anthropic when invoked through OpenCode; any provider and model
|
|
70
90
|
supported by the user's OpenCode configuration can execute the commands.
|
|
@@ -74,19 +94,54 @@ supported by the user's OpenCode configuration can execute the commands.
|
|
|
74
94
|
The adapter follows these rules:
|
|
75
95
|
|
|
76
96
|
- No global ForgeDock `instructions` entry.
|
|
77
|
-
-
|
|
78
|
-
|
|
79
|
-
|
|
97
|
+
- Top-level user entry commands are registered under `/forge/*`.
|
|
98
|
+
- Every eligible workflow is registered as a native skill so nested
|
|
99
|
+
`Skill(...)` dispatch resolves through OpenCode's `skill` tool instead of
|
|
100
|
+
filesystem guessing.
|
|
80
101
|
- Nested phase specs are loaded only when their dispatcher reaches them.
|
|
81
102
|
- Task/Agent work uses OpenCode subagents only for the parallelism, isolation,
|
|
82
103
|
or context-pressure cases required by the shared workflow.
|
|
104
|
+
- Orchestration dispatches independent issues with `task(background=true)` and
|
|
105
|
+
processes each injected task-result event immediately; it does not wait for a
|
|
106
|
+
wave or the slowest sibling.
|
|
107
|
+
- The `/orchestrate` entrypoint reads only runtime config, then runs
|
|
108
|
+
`bin/orchestrate-preflight.mjs` before loading the phase specs. The helper batches the initial GitHub
|
|
109
|
+
snapshot and emits only the compact ready queue needed for the first native task.
|
|
110
|
+
Interactive runs keep the confirmation gate; `--auto` or `--confirm` is the
|
|
111
|
+
explicit headless authorization.
|
|
112
|
+
- The full Phase 3/4 prose is reserved for investigations, unsupported or multi-repo
|
|
113
|
+
queries, explicit deep planning, and recovery after a task-result event. It remains
|
|
114
|
+
authoritative for cascade handling, leases, cleanup, and reporting.
|
|
83
115
|
- GitHub state and the durable engine remain the recovery source instead of
|
|
84
116
|
replaying prior prompt context.
|
|
85
117
|
|
|
86
118
|
The generated adapter preamble is intentionally small. It maps Claude Code's
|
|
87
|
-
in-conversation `Skill(...)` loading to
|
|
88
|
-
spec. It maps isolated `Task(...)` and permitted
|
|
89
|
-
OpenCode's native `task` tool.
|
|
119
|
+
in-conversation `Skill(...)` loading to the normalized native skill name and
|
|
120
|
+
authoritative shared spec. It maps isolated `Task(...)` and permitted
|
|
121
|
+
`Agent(...)` calls to OpenCode's native `task` tool. Before the shared review
|
|
122
|
+
specs evaluate Claude's literal `Task`/`Agent` names, an OpenCode runtime marker
|
|
123
|
+
selects `DISPATCH_TOOL=task`; the absence of those Claude names must not produce
|
|
124
|
+
a false `FORGE:REVIEW_BLOCKED` result.
|
|
125
|
+
|
|
126
|
+
Every isolated review dispatch uses a foreground native task. Every
|
|
127
|
+
native task call must include the schema-required `subagent_type` at the top
|
|
128
|
+
level alongside its `description` and `prompt`:
|
|
129
|
+
|
|
130
|
+
```js
|
|
131
|
+
{ description: "...", prompt: "...", subagent_type: "general", background: false }
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Implementation and review work uses `general`; read-only discovery uses
|
|
135
|
+
`explore`. Claude `general-purpose` maps to `general` and
|
|
136
|
+
`codebase-explorer` maps to `explore`. When a source task omits its type, the
|
|
137
|
+
generated adapter safely defaults it to `general`; unsupported types stop with
|
|
138
|
+
`FORGE_OPENCODE_CAPABILITY_ERROR` instead of reaching schema validation.
|
|
139
|
+
If the native `task` capability itself is unavailable, the workflow posts
|
|
140
|
+
`FORGE:REVIEW_BLOCKED` and stops rather than falling back to inline review or
|
|
141
|
+
another pipeline controller.
|
|
142
|
+
The generated plugin defaults omitted `background` to `false`. Only the
|
|
143
|
+
orchestrator's ready-issue dispatcher sets `background: true`; setting
|
|
144
|
+
`OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=false` forces foreground execution.
|
|
90
145
|
|
|
91
146
|
`commands/work-on.md` is still a large entry dispatcher and is loaded in full,
|
|
92
147
|
matching the Claude Code path. The adapter prevents additional eager loading,
|
|
@@ -102,13 +157,27 @@ npx forgedock opencode install --extras
|
|
|
102
157
|
npx forgedock opencode uninstall
|
|
103
158
|
```
|
|
104
159
|
|
|
160
|
+
`npx forgedock update` also refreshes an existing managed OpenCode adapter,
|
|
161
|
+
including its plugin and generated skills. It preserves the installed core vs.
|
|
162
|
+
`--extras` tier, migrates the sentinel-marked legacy adapter, and does not
|
|
163
|
+
install OpenCode files when no ForgeDock adapter is already registered.
|
|
164
|
+
|
|
105
165
|
Updates are deterministic and prune stale ForgeDock-owned command files.
|
|
106
166
|
Uninstall removes only files listed in the ownership manifest and still marked
|
|
107
167
|
with a ForgeDock sentinel. User-owned commands, plugins, and files placed in
|
|
108
168
|
the `forgedock/` namespace are never removed; that namespace is pruned only
|
|
109
169
|
when it is empty.
|
|
110
|
-
|
|
111
|
-
|
|
170
|
+
For backward compatibility, install and uninstall may rewrite `opencode.json`
|
|
171
|
+
only to remove legacy entries whose two-key `description` and `template` still
|
|
172
|
+
exactly match one of the definitions emitted by the retired adapter and point to
|
|
173
|
+
the active ForgeDock home (or the home recorded in the ownership manifest during
|
|
174
|
+
uninstall), and the sentinel-marked legacy instructions file is present. It also
|
|
175
|
+
removes references to that managed `~/.opencode-forge.md` instructions file. The
|
|
176
|
+
migration does not rewrite `opencode.jsonc`. User-owned settings and customized commands are preserved;
|
|
177
|
+
definitions with extra keys, edited fields, different
|
|
178
|
+
paths, a missing ownership marker, or ambiguous ownership are left in place. If
|
|
179
|
+
a legacy config cannot be parsed or written, the migration leaves the legacy
|
|
180
|
+
artifacts in place.
|
|
112
181
|
|
|
113
182
|
## Locations
|
|
114
183
|
|
|
@@ -117,6 +186,7 @@ Default global location:
|
|
|
117
186
|
```text
|
|
118
187
|
~/.config/opencode/
|
|
119
188
|
commands/forge/*.md
|
|
189
|
+
skills/<workflow-name>/SKILL.md
|
|
120
190
|
plugins/forgedock.js
|
|
121
191
|
forgedock/manifest.json
|
|
122
192
|
```
|
|
@@ -133,25 +203,43 @@ The separate `forgedock run` and `forgedock run-issue` backend still supports
|
|
|
133
203
|
only Claude CLI and the Anthropic API. An OpenCode engine backend must be added
|
|
134
204
|
and validated before ForgeDock can claim provider-neutral headless parity.
|
|
135
205
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
206
|
+
The ForgeDock plugin provides the OpenCode-specific runtime boundary for shell
|
|
207
|
+
and task execution. Its `tool.execute.before` hook rejects Claude-backed and
|
|
208
|
+
recursive ForgeDock controller commands, and normalizes native task arguments
|
|
209
|
+
before execution; the shared workflow rules and deterministic scripts remain
|
|
210
|
+
the source of truth for all other behavior.
|
|
211
|
+
|
|
212
|
+
OpenCode 1.18.4 keeps background subagents experimental. ForgeDock opts into
|
|
213
|
+
that feature by default because the streaming DAG depends on the parent session
|
|
214
|
+
receiving one completion event per child. A native background task returns a
|
|
215
|
+
`<task id="..." state="running">` marker immediately and later injects a
|
|
216
|
+
`state="completed"` or `state="error"` result into the parent session. The
|
|
217
|
+
orchestrator maps that id to the issue, re-reads GitHub state, and dispatches
|
|
218
|
+
newly unlocked successors in the same response.
|
|
219
|
+
|
|
220
|
+
If `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=false` is explicitly set,
|
|
221
|
+
ForgeDock uses independent foreground tasks where OpenCode can execute them
|
|
222
|
+
concurrently and labels the run as degraded. Foreground tasks cannot provide
|
|
223
|
+
the same per-completion wake behavior, so this opt-out intentionally restores a
|
|
224
|
+
wave-like fallback rather than silently claiming Claude-equivalent throughput.
|
|
146
225
|
|
|
147
226
|
Commands that inspect Claude-specific transcripts or Claude installation state
|
|
148
227
|
remain runtime-specific and should not be represented as portable until they
|
|
149
228
|
receive dedicated implementations.
|
|
150
229
|
|
|
230
|
+
The preflight is intentionally bounded. It handles explicit issue sets and the
|
|
231
|
+
common single-repository `fast-lane`, `milestone`, `next`, `priority`, and
|
|
232
|
+
`no:milestone` queries, plus explicit dependencies and scoped issue-body file
|
|
233
|
+
overlap. Unsupported or complex inputs fall back to the shared phase specs rather
|
|
234
|
+
than silently weakening their safety rules.
|
|
235
|
+
|
|
151
236
|
## Orchestration Runtime
|
|
152
237
|
|
|
153
238
|
OpenCode orchestration uses the native OpenCode `task` tool for isolated issue
|
|
154
|
-
work and follows the shared `commands/work-on.md` state machine.
|
|
239
|
+
work and follows the shared `commands/work-on.md` state machine. Each ready
|
|
240
|
+
issue is launched as `task(subagent_type="general", background=true)` and its
|
|
241
|
+
task-result event is treated as the completion notification for that issue. It
|
|
242
|
+
must not
|
|
155
243
|
route through `forgedock run-issue` when that would select the Claude CLI or
|
|
156
244
|
Anthropic API backend. If the host does not expose an OpenCode runtime marker,
|
|
157
245
|
set the runtime explicitly before launching a headless command:
|
|
@@ -162,10 +250,25 @@ FORGE_RUNTIME=opencode opencode run --command forge/orchestrate "fast-lane"
|
|
|
162
250
|
|
|
163
251
|
Each task re-reads GitHub labels and `FORGE:*` comments after every phase and
|
|
164
252
|
continues until `workflow:merged`, `workflow:invalid`, `needs-human`, or
|
|
165
|
-
`workflow:awaiting-merge`.
|
|
253
|
+
`workflow:awaiting-merge`. The parent orchestrator processes every child
|
|
254
|
+
completion independently, so a completed predecessor can unlock and dispatch
|
|
255
|
+
its successors while unrelated issues continue running. If native task
|
|
256
|
+
dispatch is unavailable, post a
|
|
166
257
|
`FORGE:OPENCODE_BLOCKED` diagnostic naming the missing capability and add
|
|
167
258
|
`needs-human`; do not leave an issue stranded at `workflow:engine-error`.
|
|
168
259
|
|
|
260
|
+
If a generated command or skill cannot load its authoritative workflow, it must
|
|
261
|
+
stop with an actionable `FORGE_OPENCODE_CAPABILITY_ERROR`. It must not recover
|
|
262
|
+
by invoking `forgedock run-issue`, `npx forgedock run-issue`, or a recursive
|
|
263
|
+
`opencode run`; those paths select a competing controller or Claude-backed
|
|
264
|
+
backend.
|
|
265
|
+
|
|
266
|
+
The same boundary is enforced below the plugin for direct CLI callers. With
|
|
267
|
+
`FORGE_RUNTIME=opencode`, `forgedock run`, `forgedock run-issue`, and Claude
|
|
268
|
+
backend preflight fail before selecting a provider. The stable error is
|
|
269
|
+
`FORGE_OPENCODE_CAPABILITY_ERROR`; continue by using the registered native
|
|
270
|
+
Skill and Task workflow instead.
|
|
271
|
+
|
|
169
272
|
This runtime branch is additive. Claude keeps its existing engine and
|
|
170
273
|
background-agent paths, and Codex keeps its installed namespaced skills and
|
|
171
274
|
repo-local adapters.
|
|
@@ -122,6 +122,12 @@ Every finding must include:
|
|
|
122
122
|
|
|
123
123
|
**All review agents MUST include a machine-readable findings block at the end of their PR comment.** This is NON-OPTIONAL. Without structured findings, the review system cannot create GitHub issues, and findings die as unread PR comments. Every finding that doesn't become a GitHub issue is a finding that will never be addressed.
|
|
124
124
|
|
|
125
|
+
### Persist Before Post
|
|
126
|
+
|
|
127
|
+
GitHub is a delivery channel, not the sole record of a review. Before the first `gh pr comment` attempt, write the finalized complete review body, including its `<!-- FORGE:REVIEW-AGENT:{domain} -->` marker and structured findings block, to a uniquely named durable file such as `${TMPDIR:-/tmp}/forge-review-${PR_NUMBER}-${DOMAIN}-$$.md`. Use `gh pr comment --body-file "$REVIEW_BODY_PATH"`; do not construct a retry loop around a failed write.
|
|
128
|
+
|
|
129
|
+
Every agent MUST return its verdict, finding count, and one line per finding to the orchestrator even when posting succeeds. If the post fails, return the durable body path and the same finding summary, then stop. A 403 or other write failure is a failed delivery, not a clean review and not a reason to retry in the background.
|
|
130
|
+
|
|
125
131
|
### Format
|
|
126
132
|
|
|
127
133
|
Append this block at the very end of your comment (after the `---` footer line, still inside the EOF heredoc). It uses HTML comments so it's invisible in rendered markdown:
|
|
@@ -140,6 +146,7 @@ Append this block at the very end of your comment (after the `---` footer line,
|
|
|
140
146
|
6. **Summary**: Concise one-line description (no pipe `|` characters in summary)
|
|
141
147
|
7. **Empty block**: If no findings at all, include just the START/END markers
|
|
142
148
|
8. **HTML comments**: The block is invisible in rendered markdown but parseable by the review system
|
|
149
|
+
9. **Agent marker**: Include exactly one `<!-- FORGE:REVIEW-AGENT:{domain} -->` marker in the persisted body, where `{domain}` is the lowercase dispatched domain.
|
|
143
150
|
|
|
144
151
|
### Domain Prefixes
|
|
145
152
|
|
package/package.json
CHANGED
|
@@ -47,6 +47,7 @@ function parseArmBody(body) {
|
|
|
47
47
|
};
|
|
48
48
|
return {
|
|
49
49
|
maxGeneration: get("PRESET_MAX_GEN"),
|
|
50
|
+
batchMaxGeneration: get("PRESET_BATCH_MAX_GEN"),
|
|
50
51
|
tokenBudget: get("PRESET_TOKEN_BUDGET"),
|
|
51
52
|
deferOnBatchGated: get("PRESET_DEFER_GATED"),
|
|
52
53
|
keywordHeuristic: get("PRESET_KEYWORD"),
|
|
@@ -120,7 +120,7 @@ class_a_scan() {
|
|
|
120
120
|
# accumulated violation silently. (forge#2289 added the equivalent
|
|
121
121
|
# end-of-file flush for Class B only; this closes the same gap in Class A.)
|
|
122
122
|
flush_a_block() {
|
|
123
|
-
if [ "$HAS_GIST" -eq 1 ] &&
|
|
123
|
+
if [ "$HAS_GIST" -eq 1 ] && [[ "$BLOCK_LINES" =~ (^|$'\n')[[:space:]]*--public([[:space:]]|$) ]]; then
|
|
124
124
|
# Find the line number of --public within the block
|
|
125
125
|
PUBLIC_LN=$(echo "$BLOCK_LINES" | grep -n '^[[:space:]]*--public' | head -1 | cut -d: -f1)
|
|
126
126
|
ACTUAL_LINE=$((BLOCK_START + PUBLIC_LN))
|
|
@@ -149,9 +149,9 @@ class_a_scan() {
|
|
|
149
149
|
# content; forge#2288 adds true depth so 2+ nesting levels close correctly.)
|
|
150
150
|
IS_FENCE=0
|
|
151
151
|
IS_BARE=0
|
|
152
|
-
if
|
|
152
|
+
if [[ "$line" =~ ^[[:space:]]*${FENCE} ]]; then
|
|
153
153
|
IS_FENCE=1
|
|
154
|
-
if
|
|
154
|
+
if [[ "$line" =~ ^[[:space:]]*${FENCE}[[:space:]]*$ ]]; then
|
|
155
155
|
IS_BARE=1
|
|
156
156
|
fi
|
|
157
157
|
fi
|
|
@@ -188,7 +188,7 @@ class_a_scan() {
|
|
|
188
188
|
BLOCK_LINES="${BLOCK_LINES}${line}
|
|
189
189
|
"
|
|
190
190
|
# Check if this line has gh gist create/edit (even with line continuation \)
|
|
191
|
-
if
|
|
191
|
+
if [[ "$line" =~ gh[[:space:]]+gist[[:space:]]+(create|edit) ]]; then
|
|
192
192
|
HAS_GIST=1
|
|
193
193
|
fi
|
|
194
194
|
done < "$file"
|
|
@@ -296,9 +296,9 @@ else
|
|
|
296
296
|
# 2+ nesting levels close correctly.)
|
|
297
297
|
IS_FENCE=0
|
|
298
298
|
IS_BARE=0
|
|
299
|
-
if
|
|
299
|
+
if [[ "$line" =~ ^[[:space:]]*${FENCE} ]]; then
|
|
300
300
|
IS_FENCE=1
|
|
301
|
-
if
|
|
301
|
+
if [[ "$line" =~ ^[[:space:]]*${FENCE}[[:space:]]*$ ]]; then
|
|
302
302
|
IS_BARE=1
|
|
303
303
|
fi
|
|
304
304
|
fi
|
|
@@ -314,8 +314,9 @@ else
|
|
|
314
314
|
# (forge#2289) and is always already at rest here, since this branch
|
|
315
315
|
# is only reached when IN_CB is 0 — i.e. any block that was open has
|
|
316
316
|
# already been closed and flushed at its own fence-close below.
|
|
317
|
-
if
|
|
318
|
-
heading
|
|
317
|
+
if [[ "$line" =~ ^#{1,6}[[:space:]]+(.*)$ ]]; then
|
|
318
|
+
heading="${BASH_REMATCH[1]}"
|
|
319
|
+
heading="${heading%"${heading##*[![:space:]]}"}"
|
|
319
320
|
SECTION="$heading"
|
|
320
321
|
continue
|
|
321
322
|
fi
|
|
@@ -338,18 +339,18 @@ else
|
|
|
338
339
|
fi
|
|
339
340
|
|
|
340
341
|
# Content line while inside (at any depth).
|
|
341
|
-
if
|
|
342
|
+
if [[ "$line" == *"$ALLOWLIST_TOKEN"* ]]; then continue; fi
|
|
342
343
|
|
|
343
344
|
# Check for guard — scoped to THIS code block only (forge#2289)
|
|
344
|
-
|
|
345
|
+
[[ "$line" =~ $GUARD_PATTERN ]] && BLOCK_HAS_GUARD=1
|
|
345
346
|
|
|
346
347
|
# Check if this line is in the diff's added lines AND has a side-effect verb
|
|
347
|
-
if [ "$BLOCK_HAS_ADDED_SE" -eq 0 ] &&
|
|
348
|
+
if [ "$BLOCK_HAS_ADDED_SE" -eq 0 ] && [[ "$line" =~ $SIDE_EFFECT_PATTERN ]]; then
|
|
348
349
|
# Is this specific line in the added content?
|
|
349
|
-
if
|
|
350
|
+
if [[ "$ADDED_CONTENT" == *"${line:0:80}"* ]]; then
|
|
350
351
|
BLOCK_HAS_ADDED_SE=1
|
|
351
352
|
BLOCK_SE_LINE=$LN
|
|
352
|
-
BLOCK_SE_VERB
|
|
353
|
+
BLOCK_SE_VERB="${BASH_REMATCH[0]:-side-effect}"
|
|
353
354
|
fi
|
|
354
355
|
fi
|
|
355
356
|
done < "$file"
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
3
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
#
|
|
5
|
+
# check-shell-portability.sh — Verify shipped shell scripts have an explicit
|
|
6
|
+
# Bash interpreter and parse successfully before a quality gate executes them.
|
|
7
|
+
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
|
|
10
|
+
SCRIPTS_DIR="${1:-./scripts}"
|
|
11
|
+
|
|
12
|
+
if [ ! -d "$SCRIPTS_DIR" ]; then
|
|
13
|
+
echo "ERROR: scripts directory not found: $SCRIPTS_DIR" >&2
|
|
14
|
+
exit 2
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
violations=0
|
|
18
|
+
while IFS= read -r -d '' script; do
|
|
19
|
+
first_line=$(IFS= read -r line < "$script" || true; printf '%s' "$line")
|
|
20
|
+
if [ "$first_line" != "#!/usr/bin/env bash" ]; then
|
|
21
|
+
echo "HIGH | $script | shell scripts must declare #!/usr/bin/env bash" >&2
|
|
22
|
+
violations=$((violations + 1))
|
|
23
|
+
continue
|
|
24
|
+
fi
|
|
25
|
+
if ! bash -n "$script"; then
|
|
26
|
+
echo "HIGH | $script | shell syntax validation failed" >&2
|
|
27
|
+
violations=$((violations + 1))
|
|
28
|
+
fi
|
|
29
|
+
done < <(find "$SCRIPTS_DIR" -type f -name '*.sh' -print0 | sort -z)
|
|
30
|
+
|
|
31
|
+
if [ "$violations" -gt 0 ]; then
|
|
32
|
+
echo "check-shell-portability: $violations violation(s) found. See stderr for details." >&2
|
|
33
|
+
exit 1
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
echo "OK: Shell scripts declare Bash and pass syntax validation"
|