forge-workflow 0.0.4 → 0.0.6
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/.claude/commands/dev.md +345 -340
- package/.claude/commands/plan.md +566 -521
- package/.claude/commands/premerge.md +186 -176
- package/.claude/commands/research.md +42 -42
- package/.claude/commands/review.md +448 -442
- package/.claude/commands/rollback.md +721 -721
- package/.claude/commands/ship.md +212 -164
- package/.claude/commands/sonarcloud.md +152 -152
- package/.claude/commands/status.md +90 -48
- package/.claude/commands/validate.md +288 -282
- package/.claude/commands/verify.md +269 -221
- package/.claude/rules/greptile-review-process.md +285 -285
- package/.claude/rules/workflow.md +121 -105
- package/.claude/scripts/greptile-resolve.sh +558 -526
- package/.claude/scripts/load-env.sh +32 -32
- package/.cline/workflows/dev.md +342 -337
- package/.cline/workflows/plan.md +563 -518
- package/.cline/workflows/premerge.md +183 -173
- package/.cline/workflows/research.md +39 -39
- package/.cline/workflows/review.md +445 -439
- package/.cline/workflows/rollback.md +718 -718
- package/.cline/workflows/ship.md +209 -161
- package/.cline/workflows/sonarcloud.md +146 -146
- package/.cline/workflows/status.md +87 -45
- package/.cline/workflows/validate.md +285 -279
- package/.cline/workflows/verify.md +266 -218
- package/.codex/config.toml +11 -11
- package/.codex/skills/dev/SKILL.md +345 -340
- package/.codex/skills/plan/SKILL.md +566 -521
- package/.codex/skills/premerge/SKILL.md +186 -176
- package/.codex/skills/research/SKILL.md +42 -42
- package/.codex/skills/review/SKILL.md +448 -442
- package/.codex/skills/rollback/SKILL.md +721 -721
- package/.codex/skills/ship/SKILL.md +212 -164
- package/.codex/skills/sonarcloud/SKILL.md +149 -149
- package/.codex/skills/status/SKILL.md +90 -48
- package/.codex/skills/validate/SKILL.md +288 -282
- package/.codex/skills/verify/SKILL.md +269 -221
- package/.cursor/commands/dev.md +342 -337
- package/.cursor/commands/plan.md +563 -518
- package/.cursor/commands/premerge.md +183 -173
- package/.cursor/commands/research.md +39 -39
- package/.cursor/commands/review.md +445 -439
- package/.cursor/commands/rollback.md +718 -718
- package/.cursor/commands/ship.md +209 -161
- package/.cursor/commands/sonarcloud.md +146 -146
- package/.cursor/commands/status.md +87 -45
- package/.cursor/commands/validate.md +285 -279
- package/.cursor/commands/verify.md +266 -218
- package/.cursor/rules/permissions-guidance.mdc +37 -37
- package/.forge/hooks/check-tdd.js +240 -240
- package/.github/PLUGIN_TEMPLATE.json +32 -32
- package/.github/prompts/dev.prompt.md +347 -342
- package/.github/prompts/plan.prompt.md +568 -523
- package/.github/prompts/premerge.prompt.md +188 -178
- package/.github/prompts/research.prompt.md +44 -44
- package/.github/prompts/review.prompt.md +450 -444
- package/.github/prompts/rollback.prompt.md +723 -723
- package/.github/prompts/ship.prompt.md +214 -166
- package/.github/prompts/sonarcloud.prompt.md +151 -151
- package/.github/prompts/status.prompt.md +92 -50
- package/.github/prompts/validate.prompt.md +290 -284
- package/.github/prompts/verify.prompt.md +271 -223
- package/.github/workflows/beads-to-github.yml +56 -0
- package/.github/workflows/github-to-beads.yml +97 -0
- package/.kilocode/workflows/dev.md +346 -341
- package/.kilocode/workflows/plan.md +567 -522
- package/.kilocode/workflows/premerge.md +187 -177
- package/.kilocode/workflows/research.md +43 -43
- package/.kilocode/workflows/review.md +449 -443
- package/.kilocode/workflows/rollback.md +722 -722
- package/.kilocode/workflows/ship.md +213 -165
- package/.kilocode/workflows/sonarcloud.md +150 -150
- package/.kilocode/workflows/status.md +91 -49
- package/.kilocode/workflows/validate.md +289 -283
- package/.kilocode/workflows/verify.md +270 -222
- package/.mcp.json.example +12 -12
- package/.opencode/commands/dev.md +345 -340
- package/.opencode/commands/plan.md +566 -521
- package/.opencode/commands/premerge.md +186 -176
- package/.opencode/commands/research.md +42 -42
- package/.opencode/commands/review.md +448 -442
- package/.opencode/commands/rollback.md +721 -721
- package/.opencode/commands/ship.md +212 -164
- package/.opencode/commands/sonarcloud.md +149 -149
- package/.opencode/commands/status.md +90 -48
- package/.opencode/commands/validate.md +288 -282
- package/.opencode/commands/verify.md +269 -221
- package/.roo/commands/dev.md +346 -341
- package/.roo/commands/plan.md +567 -522
- package/.roo/commands/premerge.md +187 -177
- package/.roo/commands/research.md +43 -43
- package/.roo/commands/review.md +449 -443
- package/.roo/commands/rollback.md +722 -722
- package/.roo/commands/ship.md +213 -165
- package/.roo/commands/sonarcloud.md +150 -150
- package/.roo/commands/status.md +91 -49
- package/.roo/commands/validate.md +289 -283
- package/.roo/commands/verify.md +270 -222
- package/AGENTS.md +272 -175
- package/CLAUDE.md +110 -100
- package/README.md +429 -416
- package/bin/forge-cmd.js +317 -313
- package/bin/forge-preflight.js +322 -309
- package/bin/forge.js +4765 -4303
- package/docs/AGENT_INSTALL_PROMPT.md +342 -342
- package/docs/BEADS_GITHUB_SYNC.md +251 -251
- package/docs/ENHANCED_ONBOARDING.md +612 -602
- package/docs/EXAMPLES.md +482 -482
- package/docs/GREPTILE_SETUP.md +400 -400
- package/docs/MANUAL_REVIEW_GUIDE.md +106 -106
- package/docs/ROADMAP.md +359 -359
- package/docs/SETUP.md +663 -631
- package/docs/TOOLCHAIN.md +653 -630
- package/docs/VALIDATION.md +363 -363
- package/install.sh +40 -1056
- package/lefthook.yml +50 -39
- package/lib/agents/README.md +198 -198
- package/lib/agents/claude.plugin.json +28 -28
- package/lib/agents/cline.plugin.json +22 -22
- package/lib/agents/codex.plugin.json +19 -19
- package/lib/agents/copilot.plugin.json +24 -24
- package/lib/agents/cursor.plugin.json +25 -25
- package/lib/agents/kilocode.plugin.json +22 -22
- package/lib/agents/opencode.plugin.json +20 -20
- package/lib/agents/roo.plugin.json +23 -23
- package/lib/agents-config.js +2112 -2112
- package/lib/beads-health-check.js +143 -0
- package/lib/beads-setup.js +341 -0
- package/lib/beads-sync-scaffold.js +260 -0
- package/lib/commands/_registry.js +134 -0
- package/lib/commands/clean.js +181 -0
- package/lib/commands/dev.js +571 -513
- package/lib/commands/plan.js +692 -692
- package/lib/commands/push.js +196 -0
- package/lib/commands/recommend.js +119 -119
- package/lib/commands/ship.js +377 -377
- package/lib/commands/status.js +378 -378
- package/lib/commands/sync.js +55 -0
- package/lib/commands/team.js +37 -0
- package/lib/commands/test.js +207 -0
- package/lib/commands/validate.js +602 -602
- package/lib/commands/worktree.js +310 -0
- package/lib/context-merge.js +359 -359
- package/lib/dep-guard/analyzer.js +294 -294
- package/lib/dep-guard/behavior-detector.js +98 -98
- package/lib/dep-guard/contract-detector.js +162 -162
- package/lib/dep-guard/import-detector.js +498 -498
- package/lib/dep-guard/path-utils.js +13 -13
- package/lib/dep-guard/rubric.js +120 -120
- package/lib/dep-guard/task-parser.js +318 -318
- package/lib/detect-agent.js +191 -191
- package/lib/detect-worktree.js +47 -47
- package/lib/docs-command.js +51 -0
- package/lib/docs-copy.js +50 -0
- package/lib/file-hash.js +26 -26
- package/lib/freshness-token.js +148 -0
- package/lib/greptile-match.js +80 -0
- package/lib/husky-migration.js +450 -0
- package/lib/lefthook-check.js +65 -0
- package/lib/pat-setup.js +207 -0
- package/lib/plugin-catalog.js +350 -350
- package/lib/plugin-manager.js +166 -166
- package/lib/plugin-recommender.js +141 -141
- package/lib/project-discovery.js +491 -491
- package/lib/reset.js +309 -0
- package/lib/setup-action-log.js +139 -139
- package/lib/setup-summary-renderer.js +106 -106
- package/lib/setup-utils.js +96 -0
- package/lib/setup.js +192 -192
- package/lib/smart-merge.js +64 -0
- package/lib/symlink-utils.js +81 -0
- package/lib/task-ownership.js +117 -0
- package/lib/workflow-profiles.js +197 -197
- package/package.json +131 -128
- package/scripts/beads-context.sh +426 -0
- package/scripts/beads-context.test.js +567 -0
- package/scripts/behavioral-judge.sh +378 -0
- package/scripts/benchmark.js +85 -0
- package/scripts/branch-protection.js +183 -0
- package/scripts/check-agents.js +172 -0
- package/scripts/check-forge-token.js +98 -0
- package/scripts/commitlint.js +42 -0
- package/scripts/conflict-detect.sh +323 -0
- package/scripts/dep-guard-analyze.js +71 -0
- package/scripts/dep-guard.sh +789 -0
- package/scripts/eval_win.py +249 -0
- package/scripts/file-index.sh +493 -0
- package/scripts/forge-team/index.sh +86 -0
- package/scripts/forge-team/lib/agent-prompt.sh +52 -0
- package/scripts/forge-team/lib/claim.sh +256 -0
- package/scripts/forge-team/lib/dashboard.sh +341 -0
- package/scripts/forge-team/lib/epic.sh +332 -0
- package/scripts/forge-team/lib/hooks.sh +253 -0
- package/scripts/forge-team/lib/identity.sh +235 -0
- package/scripts/forge-team/lib/sync-github.sh +317 -0
- package/scripts/forge-team/lib/verify.sh +284 -0
- package/scripts/forge-team/lib/workload.sh +296 -0
- package/scripts/forge-team/tests/agent-prompt.test.sh +72 -0
- package/scripts/forge-team/tests/claim.test.sh +179 -0
- package/scripts/forge-team/tests/dashboard.test.sh +170 -0
- package/scripts/forge-team/tests/dispatcher.test.sh +79 -0
- package/scripts/forge-team/tests/epic.test.sh +176 -0
- package/scripts/forge-team/tests/hooks.test.sh +239 -0
- package/scripts/forge-team/tests/identity.test.sh +176 -0
- package/scripts/forge-team/tests/integration.test.sh +371 -0
- package/scripts/forge-team/tests/sync-github.test.sh +209 -0
- package/scripts/forge-team/tests/verify.test.sh +314 -0
- package/scripts/forge-team/tests/workflow-integration.test.sh +43 -0
- package/scripts/forge-team/tests/workload.test.sh +209 -0
- package/scripts/github-beads-sync/comment.mjs +64 -0
- package/scripts/github-beads-sync/config.mjs +148 -0
- package/scripts/github-beads-sync/github-api.mjs +131 -0
- package/scripts/github-beads-sync/index.mjs +332 -0
- package/scripts/github-beads-sync/label-mapper.mjs +54 -0
- package/scripts/github-beads-sync/mapping.mjs +78 -0
- package/scripts/github-beads-sync/reverse-sync-cli.mjs +31 -0
- package/scripts/github-beads-sync/reverse-sync.mjs +138 -0
- package/scripts/github-beads-sync/run-bd.mjs +159 -0
- package/scripts/github-beads-sync/sanitize.mjs +121 -0
- package/scripts/github-beads-sync.config.json +26 -0
- package/scripts/improve-command.js +375 -0
- package/scripts/lib/eval-runner.js +268 -0
- package/scripts/lib/eval-schema.js +135 -0
- package/scripts/lib/eval-storage.js +78 -0
- package/scripts/lib/grading.js +203 -0
- package/scripts/lib/jsonl-lock.sh +48 -0
- package/scripts/lib/sanitize.sh +116 -0
- package/scripts/lib/transcript-parser.js +63 -0
- package/scripts/lint.js +47 -0
- package/scripts/migrate-to-bun-test.js +412 -0
- package/scripts/pr-coordinator.sh +706 -0
- package/scripts/run-command-eval.js +236 -0
- package/scripts/smart-status.sh +809 -0
- package/scripts/sync-commands.js +571 -0
- package/scripts/sync-utils.sh +455 -0
- package/scripts/test-dashboard.js +123 -0
- package/scripts/test.js +46 -0
- package/scripts/validate.sh +94 -0
- package/skills/parallel-deep-research/SKILL.md +108 -108
- package/skills/parallel-deep-research/evals/README.md +27 -27
- package/skills/parallel-deep-research/evals/evals.json +62 -62
- package/skills/sonarcloud-analysis/SKILL.md +171 -171
- package/skills/sonarcloud-analysis/evals/README.md +27 -27
- package/skills/sonarcloud-analysis/evals/evals.json +50 -50
- package/skills/sonarcloud-analysis/references/api-reference.md +466 -466
- package/.cursor/hooks/state/continual-learning-index.json +0 -19
- package/.cursor/hooks/state/continual-learning.json +0 -8
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# file-index.sh — File index JSONL helpers for multi-developer awareness.
|
|
3
|
+
#
|
|
4
|
+
# Manages .beads/file-index.jsonl — an append-only log of which developer
|
|
5
|
+
# is working on which files/modules, keyed by issue_id.
|
|
6
|
+
#
|
|
7
|
+
# Functions (source this file):
|
|
8
|
+
# file_index_add <issue_id> <developer> <files_json> <modules_json>
|
|
9
|
+
# file_index_remove <issue_id>
|
|
10
|
+
# file_index_read
|
|
11
|
+
# file_index_get <issue_id>
|
|
12
|
+
# file_index_update_from_tasks <issue_id> <task_file_path> [action]
|
|
13
|
+
#
|
|
14
|
+
# Uses jq for all JSON construction (never string concatenation).
|
|
15
|
+
# OWASP A03: All inputs validated and shell-injection patterns stripped.
|
|
16
|
+
|
|
17
|
+
# Only set errexit/pipefail when run as a script, not when sourced
|
|
18
|
+
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
|
19
|
+
set -euo pipefail
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
# Source shared libraries
|
|
23
|
+
_FI_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
24
|
+
source "$_FI_SCRIPT_DIR/lib/sanitize.sh"
|
|
25
|
+
if [[ -f "$_FI_SCRIPT_DIR/lib/jsonl-lock.sh" ]]; then
|
|
26
|
+
source "$_FI_SCRIPT_DIR/lib/jsonl-lock.sh"
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
# ── Helpers ──────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
# Resolve the JSONL file path.
|
|
32
|
+
# FILE_INDEX_ROOT can be overridden for testing.
|
|
33
|
+
_file_index_path() {
|
|
34
|
+
local root="${FILE_INDEX_ROOT:-.}"
|
|
35
|
+
printf '%s' "$root/.beads/file-index.jsonl"
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
# Validate issue_id: alphanumeric + hyphens only
|
|
39
|
+
_validate_issue_id() {
|
|
40
|
+
local id="$1"
|
|
41
|
+
if [[ ! "$id" =~ ^[a-zA-Z0-9-]+$ ]]; then
|
|
42
|
+
echo "Error: invalid issue_id format: must be alphanumeric + hyphens only" >&2
|
|
43
|
+
return 1
|
|
44
|
+
fi
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# Validate developer identity: ^[a-zA-Z0-9._@+-]+$
|
|
48
|
+
_validate_developer() {
|
|
49
|
+
local dev="$1"
|
|
50
|
+
if [[ ! "$dev" =~ ^[a-zA-Z0-9._@+\-]+$ ]]; then
|
|
51
|
+
echo "Error: invalid developer format: must match ^[a-zA-Z0-9._@+-]+$" >&2
|
|
52
|
+
return 1
|
|
53
|
+
fi
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
# ── Public functions ─────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
# file_index_add <issue_id> <developer> <files_json> <modules_json>
|
|
59
|
+
# Append an entry to the file index JSONL.
|
|
60
|
+
# files_json and modules_json must be valid JSON arrays.
|
|
61
|
+
file_index_add() {
|
|
62
|
+
local raw_issue_id="$1"
|
|
63
|
+
local raw_developer="$2"
|
|
64
|
+
local files_json="$3"
|
|
65
|
+
local modules_json="$4"
|
|
66
|
+
|
|
67
|
+
# Sanitize inputs
|
|
68
|
+
local issue_id
|
|
69
|
+
issue_id="$(sanitize "$raw_issue_id")"
|
|
70
|
+
# Trim whitespace produced by sanitize
|
|
71
|
+
issue_id="$(printf '%s' "$issue_id" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
|
|
72
|
+
|
|
73
|
+
local developer
|
|
74
|
+
developer="$(sanitize "$raw_developer")"
|
|
75
|
+
developer="$(printf '%s' "$developer" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
|
|
76
|
+
|
|
77
|
+
# Validate formats (after sanitization — catches injection that was stripped)
|
|
78
|
+
_validate_issue_id "$issue_id" || return 1
|
|
79
|
+
_validate_developer "$developer" || return 1
|
|
80
|
+
|
|
81
|
+
# Validate files_json and modules_json are valid JSON arrays
|
|
82
|
+
if ! printf '%s' "$files_json" | jq 'if type == "array" then empty else error end' 2>/dev/null; then
|
|
83
|
+
echo "Error: files_json must be a valid JSON array" >&2
|
|
84
|
+
return 1
|
|
85
|
+
fi
|
|
86
|
+
if ! printf '%s' "$modules_json" | jq 'if type == "array" then empty else error end' 2>/dev/null; then
|
|
87
|
+
echo "Error: modules_json must be a valid JSON array" >&2
|
|
88
|
+
return 1
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
# Generate timestamp
|
|
92
|
+
local updated_at
|
|
93
|
+
updated_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
94
|
+
|
|
95
|
+
# Build JSON using jq (never string concatenation)
|
|
96
|
+
local json_line
|
|
97
|
+
json_line="$(jq -n -c \
|
|
98
|
+
--arg id "$issue_id" \
|
|
99
|
+
--arg dev "$developer" \
|
|
100
|
+
--argjson files "$files_json" \
|
|
101
|
+
--argjson modules "$modules_json" \
|
|
102
|
+
--arg ts "$updated_at" \
|
|
103
|
+
'{
|
|
104
|
+
issue_id: $id,
|
|
105
|
+
developer: $dev,
|
|
106
|
+
files: $files,
|
|
107
|
+
modules: $modules,
|
|
108
|
+
updated_at: $ts,
|
|
109
|
+
tombstone: false
|
|
110
|
+
}')"
|
|
111
|
+
|
|
112
|
+
# Append to JSONL file
|
|
113
|
+
local jsonl_path
|
|
114
|
+
jsonl_path="$(_file_index_path)"
|
|
115
|
+
|
|
116
|
+
# Ensure parent directory exists
|
|
117
|
+
mkdir -p "$(dirname "$jsonl_path")"
|
|
118
|
+
|
|
119
|
+
if command -v atomic_jsonl_append &>/dev/null; then
|
|
120
|
+
atomic_jsonl_append "$jsonl_path" "$json_line"
|
|
121
|
+
else
|
|
122
|
+
printf '%s\n' "$json_line" >> "$jsonl_path"
|
|
123
|
+
fi
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
# file_index_remove <issue_id>
|
|
127
|
+
# Append a tombstone entry for the given issue.
|
|
128
|
+
file_index_remove() {
|
|
129
|
+
local raw_issue_id="$1"
|
|
130
|
+
|
|
131
|
+
# Sanitize and validate
|
|
132
|
+
local issue_id
|
|
133
|
+
issue_id="$(sanitize "$raw_issue_id")"
|
|
134
|
+
issue_id="$(printf '%s' "$issue_id" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
|
|
135
|
+
_validate_issue_id "$issue_id" || return 1
|
|
136
|
+
|
|
137
|
+
# Generate timestamp
|
|
138
|
+
local updated_at
|
|
139
|
+
updated_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
140
|
+
|
|
141
|
+
# Build tombstone JSON using jq
|
|
142
|
+
local json_line
|
|
143
|
+
json_line="$(jq -n -c \
|
|
144
|
+
--arg id "$issue_id" \
|
|
145
|
+
--arg ts "$updated_at" \
|
|
146
|
+
'{
|
|
147
|
+
issue_id: $id,
|
|
148
|
+
developer: "",
|
|
149
|
+
files: [],
|
|
150
|
+
modules: [],
|
|
151
|
+
updated_at: $ts,
|
|
152
|
+
tombstone: true
|
|
153
|
+
}')"
|
|
154
|
+
|
|
155
|
+
# Append to JSONL file
|
|
156
|
+
local jsonl_path
|
|
157
|
+
jsonl_path="$(_file_index_path)"
|
|
158
|
+
mkdir -p "$(dirname "$jsonl_path")"
|
|
159
|
+
if command -v atomic_jsonl_append &>/dev/null; then
|
|
160
|
+
atomic_jsonl_append "$jsonl_path" "$json_line"
|
|
161
|
+
else
|
|
162
|
+
printf '%s\n' "$json_line" >> "$jsonl_path"
|
|
163
|
+
fi
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
# file_index_read
|
|
167
|
+
# Read all entries, resolve LWW per issue_id, output active entries as JSON array.
|
|
168
|
+
# Returns "[]" if the file is missing or empty.
|
|
169
|
+
file_index_read() {
|
|
170
|
+
local jsonl_path
|
|
171
|
+
jsonl_path="$(_file_index_path)"
|
|
172
|
+
|
|
173
|
+
# Handle missing or empty file
|
|
174
|
+
if [[ ! -f "$jsonl_path" ]] || [[ ! -s "$jsonl_path" ]]; then
|
|
175
|
+
printf '%s' "[]"
|
|
176
|
+
return 0
|
|
177
|
+
fi
|
|
178
|
+
|
|
179
|
+
# Use jq to:
|
|
180
|
+
# 1. Slurp all lines into an array
|
|
181
|
+
# 2. Group by issue_id
|
|
182
|
+
# 3. For each group, sort by updated_at and take the last (LWW)
|
|
183
|
+
# 4. Filter out tombstoned entries
|
|
184
|
+
jq -s -c '
|
|
185
|
+
sort_by(.issue_id)
|
|
186
|
+
| group_by(.issue_id)
|
|
187
|
+
| map(sort_by(.updated_at) | last)
|
|
188
|
+
| map(select(.tombstone == false))
|
|
189
|
+
' "$jsonl_path"
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
# file_index_get <issue_id>
|
|
193
|
+
# Get a single issue's file entry (after LWW resolution).
|
|
194
|
+
# Returns "null" if not found or tombstoned.
|
|
195
|
+
file_index_get() {
|
|
196
|
+
local raw_issue_id="$1"
|
|
197
|
+
|
|
198
|
+
# Sanitize and validate
|
|
199
|
+
local issue_id
|
|
200
|
+
issue_id="$(sanitize "$raw_issue_id")"
|
|
201
|
+
issue_id="$(printf '%s' "$issue_id" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
|
|
202
|
+
_validate_issue_id "$issue_id" || return 1
|
|
203
|
+
|
|
204
|
+
local jsonl_path
|
|
205
|
+
jsonl_path="$(_file_index_path)"
|
|
206
|
+
|
|
207
|
+
# Handle missing or empty file
|
|
208
|
+
if [[ ! -f "$jsonl_path" ]] || [[ ! -s "$jsonl_path" ]]; then
|
|
209
|
+
printf '%s' "null"
|
|
210
|
+
return 0
|
|
211
|
+
fi
|
|
212
|
+
|
|
213
|
+
# Use jq to filter by issue_id, resolve LWW, check tombstone
|
|
214
|
+
jq -s -c --arg id "$issue_id" '
|
|
215
|
+
map(select(.issue_id == $id))
|
|
216
|
+
| sort_by(.updated_at)
|
|
217
|
+
| last
|
|
218
|
+
| if . == null then null
|
|
219
|
+
elif .tombstone == true then null
|
|
220
|
+
else .
|
|
221
|
+
end
|
|
222
|
+
' "$jsonl_path"
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
# file_index_update_from_tasks <issue_id> <task_file_path> [action]
|
|
226
|
+
# Parse a task file for File(s): lines, extract file paths, derive modules,
|
|
227
|
+
# and update the file index using the current session identity.
|
|
228
|
+
#
|
|
229
|
+
# action: "in_progress" (default) — adds/updates entry
|
|
230
|
+
# "closed" — appends tombstone via file_index_remove
|
|
231
|
+
#
|
|
232
|
+
# Edge cases:
|
|
233
|
+
# - Missing task file or no File(s): lines → fallback with confidence: "low"
|
|
234
|
+
# - File paths are sanitized against shell injection (OWASP A03)
|
|
235
|
+
file_index_update_from_tasks() {
|
|
236
|
+
local raw_issue_id="$1"
|
|
237
|
+
local task_file_path="${2:-}"
|
|
238
|
+
local action="${3:-in_progress}"
|
|
239
|
+
|
|
240
|
+
# Sanitize and validate issue_id
|
|
241
|
+
local issue_id
|
|
242
|
+
issue_id="$(sanitize "$raw_issue_id")"
|
|
243
|
+
issue_id="$(printf '%s' "$issue_id" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
|
|
244
|
+
_validate_issue_id "$issue_id" || return 1
|
|
245
|
+
|
|
246
|
+
# Handle closed action: just tombstone
|
|
247
|
+
if [[ "$action" == "closed" ]]; then
|
|
248
|
+
file_index_remove "$issue_id"
|
|
249
|
+
return 0
|
|
250
|
+
fi
|
|
251
|
+
|
|
252
|
+
# Get session identity (source sync-utils.sh if get_session_identity not defined)
|
|
253
|
+
local developer
|
|
254
|
+
if ! command -v get_session_identity &>/dev/null; then
|
|
255
|
+
local script_dir
|
|
256
|
+
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
257
|
+
if [[ -f "$script_dir/sync-utils.sh" ]]; then
|
|
258
|
+
source "$script_dir/sync-utils.sh"
|
|
259
|
+
fi
|
|
260
|
+
fi
|
|
261
|
+
developer="$(get_session_identity)" || {
|
|
262
|
+
echo "Error: could not determine session identity" >&2
|
|
263
|
+
return 1
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
# Parse task file for File(s): lines
|
|
267
|
+
local -a raw_files=()
|
|
268
|
+
local confidence="high"
|
|
269
|
+
|
|
270
|
+
if [[ -z "$task_file_path" ]] || [[ ! -f "$task_file_path" ]]; then
|
|
271
|
+
# Fallback: no task file
|
|
272
|
+
confidence="low"
|
|
273
|
+
else
|
|
274
|
+
# Extract backtick-quoted paths from File(s): lines (case-insensitive)
|
|
275
|
+
local line
|
|
276
|
+
while IFS= read -r line; do
|
|
277
|
+
# Extract all backtick-quoted strings from this line
|
|
278
|
+
local path
|
|
279
|
+
while [[ "$line" =~ \`([^\`]+)\` ]]; do
|
|
280
|
+
path="${BASH_REMATCH[1]}"
|
|
281
|
+
# Strip only trailing annotations like " (extend)" at end of path
|
|
282
|
+
# Anchored to $ to avoid corrupting paths with parentheses (e.g., test(1).spec.ts)
|
|
283
|
+
path="$(printf '%s' "$path" | sed 's/[[:space:]]*([^)]*)$//')"
|
|
284
|
+
# Trim whitespace
|
|
285
|
+
path="$(printf '%s' "$path" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
|
|
286
|
+
if [[ -n "$path" ]]; then
|
|
287
|
+
raw_files+=("$path")
|
|
288
|
+
fi
|
|
289
|
+
# Remove the matched backtick segment — use fixed pattern to avoid
|
|
290
|
+
# glob interpretation of path characters like [ ] * ?
|
|
291
|
+
line="${line#*\`}" # strip up to and including the opening backtick
|
|
292
|
+
line="${line#*\`}" # strip the content and closing backtick
|
|
293
|
+
done
|
|
294
|
+
done < <(grep -i '^File(s):' "$task_file_path" 2>/dev/null || true)
|
|
295
|
+
|
|
296
|
+
# If no File(s): lines found, fall back
|
|
297
|
+
if [[ ${#raw_files[@]} -eq 0 ]]; then
|
|
298
|
+
confidence="low"
|
|
299
|
+
fi
|
|
300
|
+
fi
|
|
301
|
+
|
|
302
|
+
# Sanitize file paths, derive modules, deduplicate via jq (Bash 3.2 compatible)
|
|
303
|
+
local sanitized_paths=""
|
|
304
|
+
|
|
305
|
+
for raw_path in "${raw_files[@]}"; do
|
|
306
|
+
# OWASP A03: reject paths with shell injection patterns
|
|
307
|
+
if printf '%s' "$raw_path" | grep -qE '(\$\(|`|;|\||\&\&|\|\|)'; then
|
|
308
|
+
continue
|
|
309
|
+
fi
|
|
310
|
+
# Skip paths with only dots/slashes (directory traversal)
|
|
311
|
+
if [[ "$raw_path" =~ ^[./]+$ ]]; then
|
|
312
|
+
continue
|
|
313
|
+
fi
|
|
314
|
+
sanitized_paths="${sanitized_paths}${raw_path}"$'\n'
|
|
315
|
+
done
|
|
316
|
+
|
|
317
|
+
# Build deduplicated JSON arrays using jq (no associative arrays needed)
|
|
318
|
+
local files_json modules_json
|
|
319
|
+
if [[ -n "$sanitized_paths" ]]; then
|
|
320
|
+
# Save and restore pipefail state to avoid side effects
|
|
321
|
+
local _prev_pipefail
|
|
322
|
+
_prev_pipefail="$(set +o | grep pipefail)" || true
|
|
323
|
+
set -o pipefail
|
|
324
|
+
|
|
325
|
+
files_json="$(printf '%s' "$sanitized_paths" | grep -v '^$' | jq -R . | jq -s -c 'unique')" || {
|
|
326
|
+
eval "$_prev_pipefail"
|
|
327
|
+
echo "Error: failed to build files JSON array" >&2
|
|
328
|
+
return 1
|
|
329
|
+
}
|
|
330
|
+
modules_json="$(printf '%s' "$sanitized_paths" | grep -v '^$' | while IFS= read -r p; do
|
|
331
|
+
local d
|
|
332
|
+
d="$(dirname "$p")"
|
|
333
|
+
if [[ "$d" == "." ]]; then printf '%s\n' "./"; else printf '%s\n' "${d}/"; fi
|
|
334
|
+
done | jq -R . | jq -s -c 'unique')" || {
|
|
335
|
+
eval "$_prev_pipefail"
|
|
336
|
+
echo "Error: failed to build modules JSON array" >&2
|
|
337
|
+
return 1
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
# Restore pipefail state
|
|
341
|
+
eval "$_prev_pipefail"
|
|
342
|
+
else
|
|
343
|
+
files_json="[]"
|
|
344
|
+
modules_json="[]"
|
|
345
|
+
fi
|
|
346
|
+
|
|
347
|
+
# Build and append the entry
|
|
348
|
+
local updated_at
|
|
349
|
+
updated_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
350
|
+
|
|
351
|
+
local jsonl_path
|
|
352
|
+
jsonl_path="$(_file_index_path)"
|
|
353
|
+
mkdir -p "$(dirname "$jsonl_path")"
|
|
354
|
+
|
|
355
|
+
if [[ "$confidence" == "low" ]]; then
|
|
356
|
+
# Include confidence field for fallback entries
|
|
357
|
+
local json_line
|
|
358
|
+
json_line="$(jq -n -c \
|
|
359
|
+
--arg id "$issue_id" \
|
|
360
|
+
--arg dev "$developer" \
|
|
361
|
+
--argjson files "$files_json" \
|
|
362
|
+
--argjson modules "$modules_json" \
|
|
363
|
+
--arg ts "$updated_at" \
|
|
364
|
+
--arg conf "$confidence" \
|
|
365
|
+
'{
|
|
366
|
+
issue_id: $id,
|
|
367
|
+
developer: $dev,
|
|
368
|
+
files: $files,
|
|
369
|
+
modules: $modules,
|
|
370
|
+
updated_at: $ts,
|
|
371
|
+
tombstone: false,
|
|
372
|
+
confidence: $conf
|
|
373
|
+
}')"
|
|
374
|
+
if command -v atomic_jsonl_append &>/dev/null; then
|
|
375
|
+
atomic_jsonl_append "$jsonl_path" "$json_line"
|
|
376
|
+
else
|
|
377
|
+
printf '%s\n' "$json_line" >> "$jsonl_path"
|
|
378
|
+
fi
|
|
379
|
+
else
|
|
380
|
+
# Normal entry via file_index_add (no confidence field needed)
|
|
381
|
+
file_index_add "$issue_id" "$developer" "$files_json" "$modules_json"
|
|
382
|
+
fi
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
# file_index_auto_update <issue_id> [--from-git]
|
|
386
|
+
# Update the file index with the current set of changed files.
|
|
387
|
+
# Files come from: --from-git flag (git diff HEAD~1) or stdin.
|
|
388
|
+
file_index_auto_update() {
|
|
389
|
+
local raw_issue_id="$1"
|
|
390
|
+
shift
|
|
391
|
+
local from_git=0
|
|
392
|
+
|
|
393
|
+
# Parse flags
|
|
394
|
+
while [[ $# -gt 0 ]]; do
|
|
395
|
+
case "$1" in
|
|
396
|
+
--from-git) from_git=1; shift ;;
|
|
397
|
+
*) echo "Error: unknown flag '$1'" >&2; return 1 ;;
|
|
398
|
+
esac
|
|
399
|
+
done
|
|
400
|
+
|
|
401
|
+
# Sanitize and validate issue_id
|
|
402
|
+
local issue_id
|
|
403
|
+
issue_id="$(sanitize "$raw_issue_id")"
|
|
404
|
+
issue_id="$(printf '%s' "$issue_id" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
|
|
405
|
+
_validate_issue_id "$issue_id" || return 1
|
|
406
|
+
|
|
407
|
+
# Get file list
|
|
408
|
+
local -a files=()
|
|
409
|
+
if [[ "$from_git" -eq 1 ]]; then
|
|
410
|
+
while IFS= read -r f; do
|
|
411
|
+
[[ -n "$f" ]] && files+=("$f")
|
|
412
|
+
done < <(git diff --name-only HEAD~1 2>/dev/null || true)
|
|
413
|
+
else
|
|
414
|
+
# Read from stdin (non-blocking check)
|
|
415
|
+
if [[ ! -t 0 ]]; then
|
|
416
|
+
while IFS= read -r f; do
|
|
417
|
+
[[ -n "$f" ]] && files+=("$f")
|
|
418
|
+
done
|
|
419
|
+
else
|
|
420
|
+
echo "Error: no file list provided (use --from-git or pipe files via stdin)" >&2
|
|
421
|
+
return 1
|
|
422
|
+
fi
|
|
423
|
+
fi
|
|
424
|
+
|
|
425
|
+
# Empty file list = nothing to do
|
|
426
|
+
if [[ ${#files[@]} -eq 0 ]]; then
|
|
427
|
+
echo "No files changed"
|
|
428
|
+
return 0
|
|
429
|
+
fi
|
|
430
|
+
|
|
431
|
+
# Get session identity
|
|
432
|
+
local developer
|
|
433
|
+
if ! command -v get_session_identity &>/dev/null; then
|
|
434
|
+
local script_dir
|
|
435
|
+
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
436
|
+
if [[ -f "$script_dir/sync-utils.sh" ]]; then
|
|
437
|
+
source "$script_dir/sync-utils.sh"
|
|
438
|
+
fi
|
|
439
|
+
fi
|
|
440
|
+
developer="$(get_session_identity)" || {
|
|
441
|
+
echo "Error: could not determine session identity" >&2
|
|
442
|
+
return 1
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
# Build JSON arrays using jq
|
|
446
|
+
local files_json modules_json
|
|
447
|
+
files_json="$(printf '%s\n' "${files[@]}" | jq -R . | jq -s -c 'unique')"
|
|
448
|
+
modules_json="$(printf '%s\n' "${files[@]}" | while IFS= read -r p; do
|
|
449
|
+
local d
|
|
450
|
+
d="$(dirname "$p")"
|
|
451
|
+
if [[ "$d" == "." ]]; then printf '%s\n' "./"; else printf '%s\n' "${d}/"; fi
|
|
452
|
+
done | jq -R . | jq -s -c 'unique')"
|
|
453
|
+
|
|
454
|
+
# Append via file_index_add (which uses atomic locking)
|
|
455
|
+
file_index_add "$issue_id" "$developer" "$files_json" "$modules_json"
|
|
456
|
+
|
|
457
|
+
echo "File index updated for $issue_id (${#files[@]} files)"
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
# ── Main dispatcher (when run as a script) ────────────────────────────────
|
|
461
|
+
|
|
462
|
+
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
|
463
|
+
if [[ $# -lt 1 ]]; then
|
|
464
|
+
cat >&2 <<'EOF'
|
|
465
|
+
Usage: file-index.sh <subcommand> [args...]
|
|
466
|
+
|
|
467
|
+
Subcommands:
|
|
468
|
+
add <issue_id> <developer> <files_json> <modules_json>
|
|
469
|
+
remove <issue_id>
|
|
470
|
+
read
|
|
471
|
+
get <issue_id>
|
|
472
|
+
update-from-tasks <issue_id> <task_file_path> [action]
|
|
473
|
+
auto-update <issue_id> [--from-git]
|
|
474
|
+
EOF
|
|
475
|
+
exit 1
|
|
476
|
+
fi
|
|
477
|
+
|
|
478
|
+
subcommand="$1"
|
|
479
|
+
shift
|
|
480
|
+
|
|
481
|
+
case "$subcommand" in
|
|
482
|
+
add) file_index_add "$@" ;;
|
|
483
|
+
remove) file_index_remove "$@" ;;
|
|
484
|
+
read) file_index_read ;;
|
|
485
|
+
get) file_index_get "$@" ;;
|
|
486
|
+
update-from-tasks) file_index_update_from_tasks "$@" ;;
|
|
487
|
+
auto-update) file_index_auto_update "$@" ;;
|
|
488
|
+
*)
|
|
489
|
+
echo "Error: Unknown subcommand '${subcommand}'" >&2
|
|
490
|
+
exit 1
|
|
491
|
+
;;
|
|
492
|
+
esac
|
|
493
|
+
fi
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# forge-team — Team orchestration plugin for Forge.
|
|
3
|
+
#
|
|
4
|
+
# Subcommands:
|
|
5
|
+
# workload Show team workload by developer
|
|
6
|
+
# epic Epic progress rollup
|
|
7
|
+
# dashboard Team health dashboard
|
|
8
|
+
# add Add developer to team map
|
|
9
|
+
# verify Check 1:1 Beads<>GitHub enforcement
|
|
10
|
+
# sync Manual GitHub<>Beads sync
|
|
11
|
+
# claim Claim an issue with pre-check
|
|
12
|
+
# help Show usage
|
|
13
|
+
#
|
|
14
|
+
# Exit codes: 0=success, 1=error, 2=validation error
|
|
15
|
+
|
|
16
|
+
set -euo pipefail
|
|
17
|
+
|
|
18
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
19
|
+
|
|
20
|
+
# Source libs (graceful if missing — stubs used in early dev)
|
|
21
|
+
[[ -f "$SCRIPT_DIR/lib/agent-prompt.sh" ]] && source "$SCRIPT_DIR/lib/agent-prompt.sh"
|
|
22
|
+
[[ -f "$SCRIPT_DIR/lib/identity.sh" ]] && source "$SCRIPT_DIR/lib/identity.sh"
|
|
23
|
+
[[ -f "$SCRIPT_DIR/lib/workload.sh" ]] && source "$SCRIPT_DIR/lib/workload.sh"
|
|
24
|
+
[[ -f "$SCRIPT_DIR/lib/epic.sh" ]] && source "$SCRIPT_DIR/lib/epic.sh"
|
|
25
|
+
[[ -f "$SCRIPT_DIR/lib/dashboard.sh" ]] && source "$SCRIPT_DIR/lib/dashboard.sh"
|
|
26
|
+
[[ -f "$SCRIPT_DIR/lib/hooks.sh" ]] && source "$SCRIPT_DIR/lib/hooks.sh"
|
|
27
|
+
[[ -f "$SCRIPT_DIR/lib/verify.sh" ]] && source "$SCRIPT_DIR/lib/verify.sh"
|
|
28
|
+
[[ -f "$SCRIPT_DIR/lib/claim.sh" ]] && source "$SCRIPT_DIR/lib/claim.sh"
|
|
29
|
+
|
|
30
|
+
# Source shared forge utilities
|
|
31
|
+
FORGE_SCRIPTS="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
32
|
+
[[ -f "$FORGE_SCRIPTS/lib/sanitize.sh" ]] && source "$FORGE_SCRIPTS/lib/sanitize.sh"
|
|
33
|
+
|
|
34
|
+
# ── Stub implementations ──
|
|
35
|
+
# cmd_workload provided by lib/workload.sh
|
|
36
|
+
# cmd_epic provided by lib/epic.sh
|
|
37
|
+
# cmd_dashboard provided by lib/dashboard.sh
|
|
38
|
+
cmd_add() { auto_detect_identity "$@"; }
|
|
39
|
+
# cmd_verify provided by lib/verify.sh
|
|
40
|
+
cmd_sync() { forge_team_sync "$@"; }
|
|
41
|
+
cmd_claim() { forge_team_claim "$@"; }
|
|
42
|
+
|
|
43
|
+
cmd_help() {
|
|
44
|
+
cat <<'EOF'
|
|
45
|
+
Usage: forge team <subcommand> [args...]
|
|
46
|
+
|
|
47
|
+
Subcommands:
|
|
48
|
+
workload [--developer=<user>] [--me] Show team workload
|
|
49
|
+
epic <issue-id> Epic progress rollup
|
|
50
|
+
dashboard Team health dashboard
|
|
51
|
+
add [--github=<user>] Add developer to team map
|
|
52
|
+
verify Check 1:1 Beads<>GitHub sync
|
|
53
|
+
sync Manual GitHub<>Beads sync
|
|
54
|
+
claim <issue-id> [--force] Claim issue with pre-check
|
|
55
|
+
help Show this message
|
|
56
|
+
EOF
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# ── Main dispatcher ──
|
|
60
|
+
main() {
|
|
61
|
+
if [[ $# -lt 1 ]]; then
|
|
62
|
+
cmd_help >&2
|
|
63
|
+
exit 1
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
local subcommand="$1"
|
|
67
|
+
shift
|
|
68
|
+
|
|
69
|
+
case "$subcommand" in
|
|
70
|
+
workload) cmd_workload "$@" ;;
|
|
71
|
+
epic) cmd_epic "$@" ;;
|
|
72
|
+
dashboard) cmd_dashboard "$@" ;;
|
|
73
|
+
add) cmd_add "$@" ;;
|
|
74
|
+
verify) cmd_verify "$@" ;;
|
|
75
|
+
sync) cmd_sync "$@" ;;
|
|
76
|
+
claim) cmd_claim "$@" ;;
|
|
77
|
+
help) cmd_help ;;
|
|
78
|
+
*)
|
|
79
|
+
echo "Error: unknown subcommand '$subcommand'" >&2
|
|
80
|
+
cmd_help >&2
|
|
81
|
+
exit 1
|
|
82
|
+
;;
|
|
83
|
+
esac
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
main "$@"
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# agent-prompt.sh — Shared library for structured agent communication
|
|
2
|
+
# All user-facing output from `forge team` goes through these helpers.
|
|
3
|
+
# Uses non-guessable prefix to prevent injection from GitHub data.
|
|
4
|
+
#
|
|
5
|
+
# Usage: source this file from other scripts.
|
|
6
|
+
# This library does NOT set errexit/pipefail.
|
|
7
|
+
|
|
8
|
+
# Guard against double-sourcing (readonly would error on second source)
|
|
9
|
+
if [[ -n "${_AGENT_PROMPT_LIB_LOADED:-}" ]]; then
|
|
10
|
+
return 0 2>/dev/null || true
|
|
11
|
+
fi
|
|
12
|
+
_AGENT_PROMPT_LIB_LOADED=1
|
|
13
|
+
|
|
14
|
+
readonly FORGE_AGENT_PREFIX="FORGE_AGENT_7f3a"
|
|
15
|
+
|
|
16
|
+
# Output a prompt for the AI agent to ask the user.
|
|
17
|
+
# Format: FORGE_AGENT_7f3a:PROMPT: <message> (to stderr)
|
|
18
|
+
agent_prompt() {
|
|
19
|
+
echo "${FORGE_AGENT_PREFIX}:PROMPT: $1" >&2
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
# Informational output.
|
|
23
|
+
# Format: FORGE_AGENT_7f3a:INFO: <message> (to stderr)
|
|
24
|
+
agent_info() {
|
|
25
|
+
echo "${FORGE_AGENT_PREFIX}:INFO: $1" >&2
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
# Error output.
|
|
29
|
+
# Format: FORGE_AGENT_7f3a:ERROR: <message> (to stderr)
|
|
30
|
+
agent_error() {
|
|
31
|
+
echo "${FORGE_AGENT_PREFIX}:ERROR: $1" >&2
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# Strip any occurrence of the FORGE_AGENT_7f3a: prefix from input strings.
|
|
35
|
+
# Prevents injection from GitHub issue titles or other external data.
|
|
36
|
+
# Takes input via stdin or as $1 arg, outputs sanitized text to stdout.
|
|
37
|
+
sanitize_for_agent() {
|
|
38
|
+
local input
|
|
39
|
+
if [[ $# -gt 0 ]]; then
|
|
40
|
+
input="$1"
|
|
41
|
+
else
|
|
42
|
+
input="$(cat)"
|
|
43
|
+
fi
|
|
44
|
+
# Loop until no more prefix occurrences (prevents split-prefix bypass)
|
|
45
|
+
local prev
|
|
46
|
+
while true; do
|
|
47
|
+
prev="$input"
|
|
48
|
+
input="${input//${FORGE_AGENT_PREFIX}:/}"
|
|
49
|
+
[[ "$input" == "$prev" ]] && break
|
|
50
|
+
done
|
|
51
|
+
printf '%s' "$input"
|
|
52
|
+
}
|