forge-workflow 0.0.5 → 0.0.7
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 +6 -1
- package/.claude/commands/plan.md +59 -14
- package/.claude/commands/premerge.md +10 -0
- package/.claude/commands/review.md +7 -1
- package/.claude/commands/ship.md +95 -47
- package/.claude/commands/status.md +42 -0
- package/.claude/commands/validate.md +7 -1
- package/.claude/commands/verify.md +52 -4
- package/.claude/rules/workflow.md +16 -0
- package/.claude/scripts/greptile-resolve.sh +32 -0
- package/.cline/workflows/dev.md +6 -1
- package/.cline/workflows/plan.md +59 -14
- package/.cline/workflows/premerge.md +10 -0
- package/.cline/workflows/review.md +7 -1
- package/.cline/workflows/ship.md +95 -47
- package/.cline/workflows/status.md +42 -0
- package/.cline/workflows/validate.md +7 -1
- package/.cline/workflows/verify.md +52 -4
- package/.codex/skills/dev/SKILL.md +6 -1
- package/.codex/skills/plan/SKILL.md +59 -14
- package/.codex/skills/premerge/SKILL.md +10 -0
- package/.codex/skills/review/SKILL.md +7 -1
- package/.codex/skills/ship/SKILL.md +95 -47
- package/.codex/skills/status/SKILL.md +42 -0
- package/.codex/skills/validate/SKILL.md +7 -1
- package/.codex/skills/verify/SKILL.md +52 -4
- package/.cursor/commands/dev.md +6 -1
- package/.cursor/commands/plan.md +59 -14
- package/.cursor/commands/premerge.md +10 -0
- package/.cursor/commands/review.md +7 -1
- package/.cursor/commands/ship.md +95 -47
- package/.cursor/commands/status.md +42 -0
- package/.cursor/commands/validate.md +7 -1
- package/.cursor/commands/verify.md +52 -4
- package/.cursorrules +149 -0
- package/.github/prompts/dev.prompt.md +6 -1
- package/.github/prompts/plan.prompt.md +59 -14
- package/.github/prompts/premerge.prompt.md +10 -0
- package/.github/prompts/review.prompt.md +7 -1
- package/.github/prompts/ship.prompt.md +95 -47
- package/.github/prompts/status.prompt.md +42 -0
- package/.github/prompts/validate.prompt.md +7 -1
- package/.github/prompts/verify.prompt.md +52 -4
- package/.kilocode/workflows/dev.md +6 -1
- package/.kilocode/workflows/plan.md +59 -14
- package/.kilocode/workflows/premerge.md +10 -0
- package/.kilocode/workflows/review.md +7 -1
- package/.kilocode/workflows/ship.md +95 -47
- package/.kilocode/workflows/status.md +42 -0
- package/.kilocode/workflows/validate.md +7 -1
- package/.kilocode/workflows/verify.md +52 -4
- package/.opencode/commands/dev.md +6 -1
- package/.opencode/commands/plan.md +59 -14
- package/.opencode/commands/premerge.md +10 -0
- package/.opencode/commands/review.md +7 -1
- package/.opencode/commands/ship.md +95 -47
- package/.opencode/commands/status.md +42 -0
- package/.opencode/commands/validate.md +7 -1
- package/.opencode/commands/verify.md +52 -4
- package/.roo/commands/dev.md +6 -1
- package/.roo/commands/plan.md +59 -14
- package/.roo/commands/premerge.md +10 -0
- package/.roo/commands/review.md +7 -1
- package/.roo/commands/ship.md +95 -47
- package/.roo/commands/status.md +42 -0
- package/.roo/commands/validate.md +7 -1
- package/.roo/commands/verify.md +52 -4
- package/AGENTS.md +97 -0
- package/CLAUDE.md +10 -0
- package/README.md +2 -2
- package/bin/forge-cmd.js +5 -1
- package/bin/forge-preflight.js +15 -2
- package/bin/forge.js +211 -9
- package/docs/ENHANCED_ONBOARDING.md +96 -86
- package/docs/ROADMAP.md +2 -2
- package/docs/TOOLCHAIN.md +23 -0
- package/docs/VALIDATION.md +1 -1
- package/lefthook.yml +11 -0
- package/lib/agents/README.md +46 -1
- package/lib/agents/cline.plugin.json +11 -4
- package/lib/agents/codex.plugin.json +2 -2
- package/lib/agents/copilot.plugin.json +5 -5
- package/lib/agents/cursor.plugin.json +1 -1
- package/lib/agents/kilocode.plugin.json +1 -1
- package/lib/agents/opencode.plugin.json +7 -4
- package/lib/agents/roo.plugin.json +10 -3
- package/lib/agents-config.js +129 -81
- package/lib/codex-skills.js +50 -0
- package/lib/commands/_registry.js +173 -0
- package/lib/commands/clean.js +181 -0
- package/lib/commands/commands-reset.js +147 -0
- package/lib/commands/dev.js +84 -0
- package/lib/commands/plan.js +18 -0
- package/lib/commands/push.js +196 -0
- package/lib/commands/recommend.js +1 -1
- package/lib/commands/setup.js +4295 -0
- package/lib/commands/ship.js +20 -0
- package/lib/commands/status.js +210 -44
- package/lib/commands/sync.js +71 -0
- package/lib/commands/team.js +37 -0
- package/lib/commands/test.js +207 -0
- package/lib/commands/validate.js +13 -0
- package/lib/commands/worktree.js +310 -0
- package/lib/detect-agent.js +38 -8
- package/lib/detection-utils.js +405 -0
- package/lib/docs-command.js +51 -0
- package/lib/docs-copy.js +50 -0
- package/lib/file-utils.js +260 -0
- package/lib/forge-context.js +42 -0
- package/lib/freshness-token.js +148 -0
- package/lib/frontmatter.js +79 -0
- package/lib/greptile-match.js +80 -0
- package/lib/husky-migration.js +113 -12
- package/lib/lefthook-check.js +27 -6
- package/lib/plugin-manager.js +225 -72
- package/lib/project-discovery.js +39 -5
- package/lib/reset.js +309 -0
- package/lib/runtime-health.js +305 -0
- package/lib/shell-utils.js +50 -0
- package/lib/task-ownership.js +117 -0
- package/lib/ui-utils.js +43 -0
- package/lib/validation-utils.js +163 -0
- package/lib/workflow/enforce-stage.js +179 -0
- package/lib/workflow/stages.js +201 -0
- package/lib/workflow/state.js +332 -0
- package/opencode.json +67 -0
- package/package.json +16 -6
- package/scripts/beads-context.sh +165 -22
- package/scripts/beads-context.test.js +5 -1
- package/scripts/check-agents.js +103 -0
- package/scripts/check-forge-token.js +98 -0
- package/scripts/conflict-detect.sh +2 -2
- package/scripts/dep-guard.sh +6 -28
- package/scripts/file-index.sh +117 -23
- 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/lib/eval-runner.js +39 -0
- package/scripts/lib/jsonl-lock.sh +48 -0
- package/scripts/lib/sanitize.sh +116 -0
- package/scripts/pr-coordinator.sh +756 -0
- package/scripts/smart-status.sh +58 -21
- package/scripts/sync-commands.js +49 -20
- package/scripts/sync-utils.sh +24 -29
- package/scripts/test.js +18 -1
package/scripts/beads-context.sh
CHANGED
|
@@ -7,13 +7,18 @@
|
|
|
7
7
|
# set-acceptance <issue-id> "<criteria-text>"
|
|
8
8
|
# update-progress <issue-id> <task-num> <total> "<title>" <commit-sha> <test-count> <gate-count>
|
|
9
9
|
# parse-progress <issue-id>
|
|
10
|
-
# stage-transition <issue-id> <completed-stage> <next-stage>
|
|
10
|
+
# stage-transition <issue-id> <completed-stage> <next-stage> [--summary "..."] [--decisions "..."] [--artifacts "..."] [--next "..."] [--workflow-state "{...}"]
|
|
11
|
+
# validate <issue-id>
|
|
11
12
|
#
|
|
12
13
|
# Cross-platform: works on Windows (Git Bash), macOS, and Linux.
|
|
13
14
|
# OWASP A03: All variables properly quoted to prevent shell injection.
|
|
14
15
|
|
|
15
16
|
set -euo pipefail
|
|
16
17
|
|
|
18
|
+
# Source shared sanitize library
|
|
19
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
20
|
+
source "$SCRIPT_DIR/lib/sanitize.sh"
|
|
21
|
+
|
|
17
22
|
# ── Helpers ──────────────────────────────────────────────────────────────
|
|
18
23
|
|
|
19
24
|
usage() {
|
|
@@ -25,7 +30,8 @@ Subcommands:
|
|
|
25
30
|
set-acceptance <issue-id> "<criteria-text>"
|
|
26
31
|
update-progress <issue-id> <task-num> <total> "<title>" <commit-sha> <test-count> <gate-count>
|
|
27
32
|
parse-progress <issue-id>
|
|
28
|
-
stage-transition <issue-id> <completed-stage> <next-stage>
|
|
33
|
+
stage-transition <issue-id> <completed-stage> <next-stage> [--summary "..."] [--decisions "..."] [--artifacts "..."] [--next "..."] [--workflow-state "{...}"]
|
|
34
|
+
validate <issue-id>
|
|
29
35
|
EOF
|
|
30
36
|
exit 1
|
|
31
37
|
}
|
|
@@ -35,24 +41,6 @@ die() {
|
|
|
35
41
|
exit 1
|
|
36
42
|
}
|
|
37
43
|
|
|
38
|
-
# Sanitize a string: strip shell-injection patterns (OWASP A03)
|
|
39
|
-
# Removes: double quotes, $(...), backticks, semicolons, and newlines
|
|
40
|
-
sanitize() {
|
|
41
|
-
local val="$1"
|
|
42
|
-
# Remove double quotes
|
|
43
|
-
val="${val//\"/}"
|
|
44
|
-
# Remove $(...) command substitution patterns (loop handles nested)
|
|
45
|
-
# Use newline-separated commands for BSD sed compatibility (macOS)
|
|
46
|
-
val="$(printf '%s' "$val" | sed -e ':loop' -e 's/\$([^()]*)//g' -e 't loop')"
|
|
47
|
-
# Remove backtick command substitution
|
|
48
|
-
val="${val//\`/}"
|
|
49
|
-
# Remove semicolons (command chaining)
|
|
50
|
-
val="${val//;/}"
|
|
51
|
-
# Replace newlines with spaces
|
|
52
|
-
val="$(printf '%s' "$val" | tr '\n' ' ')"
|
|
53
|
-
printf '%s' "$val"
|
|
54
|
-
}
|
|
55
|
-
|
|
56
44
|
# Run bd update and check for errors in both exit code and output.
|
|
57
45
|
# bd update exits 0 even for non-existent issues, so we check stdout too.
|
|
58
46
|
bd_update() {
|
|
@@ -250,7 +238,7 @@ cmd_parse_progress() {
|
|
|
250
238
|
|
|
251
239
|
cmd_stage_transition() {
|
|
252
240
|
if [[ $# -lt 3 ]]; then
|
|
253
|
-
echo "Usage: beads-context.sh stage-transition <issue-id> <completed-stage> <next-stage>" >&2
|
|
241
|
+
echo "Usage: beads-context.sh stage-transition <issue-id> <completed-stage> <next-stage> [--summary \"...\"] [--decisions \"...\"] [--artifacts \"...\"] [--next \"...\"] [--workflow-state \"{...}\"]" >&2
|
|
254
242
|
exit 1
|
|
255
243
|
fi
|
|
256
244
|
|
|
@@ -259,9 +247,64 @@ cmd_stage_transition() {
|
|
|
259
247
|
completed="$(sanitize "$2")"
|
|
260
248
|
local next
|
|
261
249
|
next="$(sanitize "$3")"
|
|
262
|
-
|
|
250
|
+
shift 3
|
|
251
|
+
|
|
252
|
+
# Parse optional flags
|
|
253
|
+
local flag_summary="" flag_decisions="" flag_artifacts="" flag_next="" flag_workflow_state=""
|
|
254
|
+
while [[ $# -gt 0 ]]; do
|
|
255
|
+
case "$1" in
|
|
256
|
+
--summary)
|
|
257
|
+
shift
|
|
258
|
+
flag_summary="$(sanitize "${1:-}")"
|
|
259
|
+
;;
|
|
260
|
+
--decisions)
|
|
261
|
+
shift
|
|
262
|
+
flag_decisions="$(sanitize "${1:-}")"
|
|
263
|
+
;;
|
|
264
|
+
--artifacts)
|
|
265
|
+
shift
|
|
266
|
+
flag_artifacts="$(sanitize "${1:-}")"
|
|
267
|
+
;;
|
|
268
|
+
--next)
|
|
269
|
+
shift
|
|
270
|
+
flag_next="$(sanitize "${1:-}")"
|
|
271
|
+
;;
|
|
272
|
+
--workflow-state)
|
|
273
|
+
shift
|
|
274
|
+
flag_workflow_state="$(sanitize_config_value "${1:-}")"
|
|
275
|
+
;;
|
|
276
|
+
*)
|
|
277
|
+
# Ignore unknown flags for forward compatibility
|
|
278
|
+
;;
|
|
279
|
+
esac
|
|
280
|
+
shift
|
|
281
|
+
done
|
|
282
|
+
|
|
283
|
+
# Build comment: header line always present
|
|
263
284
|
local comment="Stage: ${completed} complete → ready for ${next}"
|
|
264
285
|
|
|
286
|
+
# Append structured fields only if provided
|
|
287
|
+
if [[ -n "$flag_summary" ]]; then
|
|
288
|
+
comment="${comment}
|
|
289
|
+
Summary: ${flag_summary}"
|
|
290
|
+
fi
|
|
291
|
+
if [[ -n "$flag_decisions" ]]; then
|
|
292
|
+
comment="${comment}
|
|
293
|
+
Decisions: ${flag_decisions}"
|
|
294
|
+
fi
|
|
295
|
+
if [[ -n "$flag_artifacts" ]]; then
|
|
296
|
+
comment="${comment}
|
|
297
|
+
Artifacts: ${flag_artifacts}"
|
|
298
|
+
fi
|
|
299
|
+
if [[ -n "$flag_next" ]]; then
|
|
300
|
+
comment="${comment}
|
|
301
|
+
Next: ${flag_next}"
|
|
302
|
+
fi
|
|
303
|
+
if [[ -n "$flag_workflow_state" ]]; then
|
|
304
|
+
comment="${comment}
|
|
305
|
+
WorkflowState: ${flag_workflow_state}"
|
|
306
|
+
fi
|
|
307
|
+
|
|
265
308
|
if ! bd_comment "$issue_id" "$comment" > /dev/null; then
|
|
266
309
|
die "Failed to record stage transition on issue ${issue_id}"
|
|
267
310
|
fi
|
|
@@ -269,6 +312,105 @@ cmd_stage_transition() {
|
|
|
269
312
|
echo "Stage transition recorded on ${issue_id}: ${completed} → ${next}"
|
|
270
313
|
}
|
|
271
314
|
|
|
315
|
+
cmd_validate() {
|
|
316
|
+
if [[ $# -lt 1 ]]; then
|
|
317
|
+
echo "Usage: beads-context.sh validate <issue-id>" >&2
|
|
318
|
+
exit 1
|
|
319
|
+
fi
|
|
320
|
+
|
|
321
|
+
local issue_id="$1"
|
|
322
|
+
local warnings=0
|
|
323
|
+
|
|
324
|
+
# Get issue JSON
|
|
325
|
+
local json
|
|
326
|
+
json="$(bd show "$issue_id" --json 2>&1)" || {
|
|
327
|
+
echo "Error: Failed to retrieve issue ${issue_id}" >&2
|
|
328
|
+
exit 1
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
# Check for bd error patterns
|
|
332
|
+
if printf '%s' "$json" | grep -Eqi '^Error|Error resolving|Error fetching'; then
|
|
333
|
+
echo "Error: Issue not found: ${issue_id}" >&2
|
|
334
|
+
exit 1
|
|
335
|
+
fi
|
|
336
|
+
|
|
337
|
+
if [[ -z "$json" || "$json" == "[]" || "$json" == "null" ]]; then
|
|
338
|
+
echo "Error: Issue not found: ${issue_id}" >&2
|
|
339
|
+
exit 1
|
|
340
|
+
fi
|
|
341
|
+
|
|
342
|
+
# Extract fields — prefer jq, fall back to grep/sed
|
|
343
|
+
local description="" design=""
|
|
344
|
+
if command -v jq &>/dev/null; then
|
|
345
|
+
description="$(printf '%s' "$json" | jq -r 'if type == "array" then .[0].description else .description end // empty' 2>/dev/null || true)"
|
|
346
|
+
design="$(printf '%s' "$json" | jq -r 'if type == "array" then .[0].design else .design end // empty' 2>/dev/null || true)"
|
|
347
|
+
else
|
|
348
|
+
description="$(printf '%s' "$json" | grep -o '"description": *"[^"]*"' | head -1 | sed 's/^"description": *"//;s/"$//' || true)"
|
|
349
|
+
design="$(printf '%s' "$json" | grep -o '"design": *"[^"]*"' | head -1 | sed 's/^"design": *"//;s/"$//' || true)"
|
|
350
|
+
fi
|
|
351
|
+
|
|
352
|
+
# Check 1: Issue has description
|
|
353
|
+
if [[ -z "$description" ]]; then
|
|
354
|
+
echo "Warning: Issue ${issue_id} has no description"
|
|
355
|
+
warnings=$((warnings + 1))
|
|
356
|
+
fi
|
|
357
|
+
|
|
358
|
+
# Get comments to check for stage transitions
|
|
359
|
+
local comments
|
|
360
|
+
comments="$(bd comments list "$issue_id" 2>/dev/null || true)"
|
|
361
|
+
|
|
362
|
+
# Check 2: At least one stage transition exists
|
|
363
|
+
local has_transition=false
|
|
364
|
+
if echo "$comments" | grep -q 'Stage:.*complete'; then
|
|
365
|
+
has_transition=true
|
|
366
|
+
fi
|
|
367
|
+
|
|
368
|
+
if [[ "$has_transition" == "false" ]]; then
|
|
369
|
+
echo "Warning: No stage transition found on issue ${issue_id}"
|
|
370
|
+
warnings=$((warnings + 1))
|
|
371
|
+
fi
|
|
372
|
+
|
|
373
|
+
# Check 3: Most recent transition has summary
|
|
374
|
+
# Isolate the last transition block to avoid false-positives from earlier summaries
|
|
375
|
+
if [[ "$has_transition" == "true" ]]; then
|
|
376
|
+
local has_summary=false
|
|
377
|
+
local last_block
|
|
378
|
+
last_block="$(echo "$comments" | awk '/Stage:.*complete/{found=1; block=""} found{block=block"\n"$0} END{print block}')"
|
|
379
|
+
if echo "$last_block" | grep -q 'Summary:'; then
|
|
380
|
+
has_summary=true
|
|
381
|
+
fi
|
|
382
|
+
if [[ "$has_summary" == "false" ]]; then
|
|
383
|
+
echo "Warning: Most recent stage transition has no summary on issue ${issue_id}"
|
|
384
|
+
warnings=$((warnings + 1))
|
|
385
|
+
fi
|
|
386
|
+
fi
|
|
387
|
+
|
|
388
|
+
# Check 4: Design metadata is set if past plan stage
|
|
389
|
+
# "Past plan" means there's a transition FROM plan or any later stage
|
|
390
|
+
local past_plan=false
|
|
391
|
+
if echo "$comments" | grep -q 'Stage: plan complete'; then
|
|
392
|
+
past_plan=true
|
|
393
|
+
fi
|
|
394
|
+
if echo "$comments" | grep -Eq 'Stage: (dev|validate|ship|review|premerge) complete'; then
|
|
395
|
+
past_plan=true
|
|
396
|
+
fi
|
|
397
|
+
|
|
398
|
+
if [[ "$past_plan" == "true" && -z "$design" ]]; then
|
|
399
|
+
echo "Warning: Design metadata not set on issue ${issue_id} (past plan stage)"
|
|
400
|
+
warnings=$((warnings + 1))
|
|
401
|
+
fi
|
|
402
|
+
|
|
403
|
+
# Summary
|
|
404
|
+
if [[ $warnings -eq 0 ]]; then
|
|
405
|
+
echo "All context fields present on issue ${issue_id}"
|
|
406
|
+
else
|
|
407
|
+
echo "${warnings} warning(s) found on issue ${issue_id}"
|
|
408
|
+
fi
|
|
409
|
+
|
|
410
|
+
# Always exit 0 — advisory only
|
|
411
|
+
exit 0
|
|
412
|
+
}
|
|
413
|
+
|
|
272
414
|
# ── Main dispatcher ──────────────────────────────────────────────────────
|
|
273
415
|
|
|
274
416
|
if [[ $# -lt 1 ]]; then
|
|
@@ -284,6 +426,7 @@ case "$subcommand" in
|
|
|
284
426
|
update-progress) cmd_update_progress "$@" ;;
|
|
285
427
|
parse-progress) cmd_parse_progress "$@" ;;
|
|
286
428
|
stage-transition) cmd_stage_transition "$@" ;;
|
|
429
|
+
validate) cmd_validate "$@" ;;
|
|
287
430
|
*)
|
|
288
431
|
echo "Error: Unknown subcommand '${subcommand}'" >&2
|
|
289
432
|
usage
|
|
@@ -8,10 +8,14 @@ const { resolveBashCommand } = require('../test/helpers/bash.js');
|
|
|
8
8
|
const SCRIPT_PATH = path.join(__dirname, 'beads-context.sh');
|
|
9
9
|
const WORKTREE_ROOT = path.resolve(__dirname, '..');
|
|
10
10
|
|
|
11
|
-
// Check if bd CLI is available (not
|
|
11
|
+
// Check if bd CLI is available AND Dolt database is reachable (not just binary exists).
|
|
12
|
+
// Prevents tests from hanging when Dolt is dead. BD_TIMEOUT env var overrides default.
|
|
12
13
|
function isBdAvailable() {
|
|
13
14
|
try {
|
|
14
15
|
execFileSync('bd', ['--version'], { stdio: 'ignore' });
|
|
16
|
+
// Also verify Dolt database is reachable (not just binary exists)
|
|
17
|
+
const timeout = parseInt(process.env.BD_TIMEOUT || '3000', 10);
|
|
18
|
+
execFileSync('bd', ['list', '--limit=1'], { stdio: 'ignore', timeout });
|
|
15
19
|
return true;
|
|
16
20
|
} catch {
|
|
17
21
|
return false;
|
package/scripts/check-agents.js
CHANGED
|
@@ -19,8 +19,110 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
const { syncCommands } = require('./sync-commands');
|
|
22
|
+
const { AGENT_ADAPTERS } = require('./sync-commands');
|
|
22
23
|
const path = require('path');
|
|
23
24
|
const fs = require('fs');
|
|
25
|
+
const { validatePluginSchema } = require('../lib/plugin-manager');
|
|
26
|
+
|
|
27
|
+
const SYNC_ADAPTER_BY_PLUGIN_ID = Object.freeze({
|
|
28
|
+
claude: 'claude-code',
|
|
29
|
+
cline: 'cline',
|
|
30
|
+
codex: 'codex',
|
|
31
|
+
copilot: 'github-copilot',
|
|
32
|
+
cursor: 'cursor',
|
|
33
|
+
kilocode: 'kilo-code',
|
|
34
|
+
opencode: 'opencode',
|
|
35
|
+
roo: 'roo-code',
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
function normalizeRelativeDir(dir) {
|
|
39
|
+
if (!dir) return null;
|
|
40
|
+
return String(dir).replace(/\\/g, '/').replace(/\/+$/, '');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getDeclaredCommandDir(plugin) {
|
|
44
|
+
const dirs = plugin.directories || {};
|
|
45
|
+
return dirs.commands || dirs.workflows || dirs.prompts || dirs.skills || null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getExpectedCommandDir(plugin) {
|
|
49
|
+
const adapterId = SYNC_ADAPTER_BY_PLUGIN_ID[plugin.id];
|
|
50
|
+
if (!adapterId) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const adapter = AGENT_ADAPTERS[adapterId];
|
|
55
|
+
if (!adapter) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (plugin.id === 'codex') {
|
|
60
|
+
return normalizeRelativeDir(plugin.directories?.skills || adapter.baseDir);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return normalizeRelativeDir(adapter.baseDir);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function validatePluginParity(plugin, errors) {
|
|
67
|
+
const validation = validatePluginSchema(plugin);
|
|
68
|
+
if (!validation.valid) {
|
|
69
|
+
const parityErrors = validation.errors.filter((error) =>
|
|
70
|
+
error.startsWith('"support') || error.startsWith('"capabilities')
|
|
71
|
+
);
|
|
72
|
+
parityErrors.forEach((error) => {
|
|
73
|
+
errors.push(`Plugin "${plugin.name || plugin.id}" (${plugin.id || 'unknown'}): ${error}`);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const capabilities = plugin.capabilities || {};
|
|
78
|
+
const dirs = plugin.directories || {};
|
|
79
|
+
const supportStatus = plugin.support?.status;
|
|
80
|
+
|
|
81
|
+
if (capabilities.commands) {
|
|
82
|
+
const expectedDir = getExpectedCommandDir(plugin);
|
|
83
|
+
const declaredDir = normalizeRelativeDir(getDeclaredCommandDir(plugin));
|
|
84
|
+
|
|
85
|
+
if (!expectedDir) {
|
|
86
|
+
errors.push(
|
|
87
|
+
`Plugin "${plugin.name}" (${plugin.id}) declares commands support but has no Forge sync adapter.`
|
|
88
|
+
);
|
|
89
|
+
} else if (!declaredDir) {
|
|
90
|
+
errors.push(
|
|
91
|
+
`Plugin "${plugin.name}" (${plugin.id}) declares commands support but has no command directory configured.`
|
|
92
|
+
);
|
|
93
|
+
} else if (declaredDir !== expectedDir) {
|
|
94
|
+
errors.push(
|
|
95
|
+
`Plugin "${plugin.name}" (${plugin.id}) command directory "${declaredDir}" does not match sync output "${expectedDir}".`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (capabilities.rules && !dirs.rules) {
|
|
101
|
+
errors.push(
|
|
102
|
+
`Plugin "${plugin.name}" (${plugin.id}) declares rules capability but has no scaffold path configured.`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (capabilities.skills && !dirs.skills) {
|
|
107
|
+
errors.push(
|
|
108
|
+
`Plugin "${plugin.name}" (${plugin.id}) declares skills capability but has no scaffold path configured.`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (supportStatus === 'deprecated' || supportStatus === 'unsupported') {
|
|
113
|
+
if (capabilities.skills) {
|
|
114
|
+
errors.push(
|
|
115
|
+
`Plugin "${plugin.name}" (${plugin.id}) is ${supportStatus} but still declares skills parity.`
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (plugin.setup?.createSkill || dirs.skills) {
|
|
120
|
+
errors.push(
|
|
121
|
+
`Plugin "${plugin.name}" (${plugin.id}) is ${supportStatus} but still scaffolds Forge skills.`
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
24
126
|
|
|
25
127
|
/**
|
|
26
128
|
* Validate all agent configs are complete and in sync.
|
|
@@ -89,6 +191,7 @@ function checkAgents(repoRoot) {
|
|
|
89
191
|
try {
|
|
90
192
|
const plugin = JSON.parse(fs.readFileSync(path.join(pluginDir, file), 'utf8'));
|
|
91
193
|
plugins.push(plugin);
|
|
194
|
+
validatePluginParity(plugin, errors);
|
|
92
195
|
} catch (_err) {
|
|
93
196
|
errors.push(`Failed to parse plugin file: lib/agents/${file}`);
|
|
94
197
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const crypto = require('node:crypto');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
|
|
7
|
+
const TOKEN_FILENAME = '.forge-push-token';
|
|
8
|
+
const MAX_AGE_MS = 30000; // 30 seconds
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get the absolute path to the token file.
|
|
12
|
+
*
|
|
13
|
+
* @param {string} projectRoot - Absolute path to project root
|
|
14
|
+
* @returns {string} Token file path
|
|
15
|
+
*/
|
|
16
|
+
function tokenPath(projectRoot) {
|
|
17
|
+
return path.join(projectRoot, TOKEN_FILENAME);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Write a one-time nonce token to disk.
|
|
22
|
+
* Called by `forge push` right before `git push` so lefthook hooks
|
|
23
|
+
* can detect that checks already passed and skip re-running them.
|
|
24
|
+
*
|
|
25
|
+
* @param {string} projectRoot - Absolute path to project root
|
|
26
|
+
* @returns {{nonce: string, timestamp: number}} The written token data
|
|
27
|
+
*/
|
|
28
|
+
function write(projectRoot) {
|
|
29
|
+
const data = {
|
|
30
|
+
nonce: crypto.randomUUID(),
|
|
31
|
+
timestamp: Date.now(),
|
|
32
|
+
};
|
|
33
|
+
fs.writeFileSync(tokenPath(projectRoot), JSON.stringify(data), 'utf-8');
|
|
34
|
+
return data;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Check if a valid (fresh, well-formed) forge push token exists.
|
|
39
|
+
* Does NOT delete the token — use `consume()` for one-time validation.
|
|
40
|
+
*
|
|
41
|
+
* @param {string} projectRoot - Absolute path to project root
|
|
42
|
+
* @returns {boolean} True if a valid, fresh token exists
|
|
43
|
+
*/
|
|
44
|
+
function isValid(projectRoot) {
|
|
45
|
+
try {
|
|
46
|
+
const raw = fs.readFileSync(tokenPath(projectRoot), 'utf-8');
|
|
47
|
+
const content = JSON.parse(raw);
|
|
48
|
+
if (!content.nonce || typeof content.timestamp !== 'number') {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
const age = Date.now() - content.timestamp;
|
|
52
|
+
return age < MAX_AGE_MS;
|
|
53
|
+
} catch (_err) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Validate and delete the token in one atomic operation (one-time use).
|
|
60
|
+
* Always attempts to delete the file, even if the token is stale or invalid.
|
|
61
|
+
*
|
|
62
|
+
* @param {string} projectRoot - Absolute path to project root
|
|
63
|
+
* @returns {boolean} True if the token was valid before deletion
|
|
64
|
+
*/
|
|
65
|
+
function consume(projectRoot) {
|
|
66
|
+
const tp = tokenPath(projectRoot);
|
|
67
|
+
try {
|
|
68
|
+
const raw = fs.readFileSync(tp, 'utf-8');
|
|
69
|
+
// Always delete after reading — whether valid or not
|
|
70
|
+
try { fs.unlinkSync(tp); } catch (_e) { /* already gone */ }
|
|
71
|
+
|
|
72
|
+
const content = JSON.parse(raw);
|
|
73
|
+
if (!content.nonce || typeof content.timestamp !== 'number') {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
const age = Date.now() - content.timestamp;
|
|
77
|
+
return age < MAX_AGE_MS;
|
|
78
|
+
} catch (_err) {
|
|
79
|
+
// File doesn't exist or isn't readable — try cleanup anyway
|
|
80
|
+
try { fs.unlinkSync(tp); } catch (_e) { /* nothing to clean */ }
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = { write, isValid, consume };
|
|
86
|
+
|
|
87
|
+
// When run directly as a script (e.g., from lefthook skip check):
|
|
88
|
+
// Exit 0 = token valid (skip hooks), Exit 1 = run hooks normally.
|
|
89
|
+
// Uses isValid() (non-destructive) so multiple lefthook commands can
|
|
90
|
+
// each check the same token. The token expires after 30s automatically.
|
|
91
|
+
if (require.main === module) {
|
|
92
|
+
const projectRoot = process.cwd();
|
|
93
|
+
if (isValid(projectRoot)) {
|
|
94
|
+
console.log('forge push token valid — skipping pre-push hooks');
|
|
95
|
+
process.exit(0);
|
|
96
|
+
}
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
@@ -19,12 +19,12 @@ set -euo pipefail
|
|
|
19
19
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
20
20
|
|
|
21
21
|
# Source file-index helpers (file_index_read, file_index_get)
|
|
22
|
-
source "$SCRIPT_DIR/file-index.sh"
|
|
22
|
+
source "$SCRIPT_DIR/file-index.sh" || { echo "FATAL: failed to source file-index.sh" >&2; exit 2; }
|
|
23
23
|
|
|
24
24
|
# Source sync-utils (get_session_identity, sanitize_config_value)
|
|
25
25
|
# Only source if available (tests may not need all utils)
|
|
26
26
|
if [[ -f "$SCRIPT_DIR/sync-utils.sh" ]]; then
|
|
27
|
-
source "$SCRIPT_DIR/sync-utils.sh"
|
|
27
|
+
source "$SCRIPT_DIR/sync-utils.sh" || { echo "FATAL: failed to source sync-utils.sh" >&2; exit 2; }
|
|
28
28
|
fi
|
|
29
29
|
|
|
30
30
|
# ── Input Validation ─────────────────────────────────────────────────────
|
package/scripts/dep-guard.sh
CHANGED
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
|
|
13
13
|
set -euo pipefail
|
|
14
14
|
|
|
15
|
+
# Source shared sanitize library
|
|
16
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
17
|
+
source "$SCRIPT_DIR/lib/sanitize.sh"
|
|
18
|
+
|
|
15
19
|
# ── Helpers ──────────────────────────────────────────────────────────────
|
|
16
20
|
|
|
17
21
|
usage() {
|
|
@@ -64,24 +68,6 @@ rollback_and_die() {
|
|
|
64
68
|
die "$message"
|
|
65
69
|
}
|
|
66
70
|
|
|
67
|
-
# Sanitize a string: strip shell-injection patterns (OWASP A03)
|
|
68
|
-
# Removes: double quotes, $(...), backticks, semicolons, and newlines
|
|
69
|
-
sanitize() {
|
|
70
|
-
local val="$1"
|
|
71
|
-
# Remove double quotes
|
|
72
|
-
val="${val//\"/}"
|
|
73
|
-
# Remove $(...) command substitution patterns (loop handles nested)
|
|
74
|
-
# Use newline-separated commands for BSD sed compatibility (macOS)
|
|
75
|
-
val="$(printf '%s' "$val" | sed -e ':loop' -e 's/\$([^()]*)//g' -e 't loop')"
|
|
76
|
-
# Remove backtick command substitution
|
|
77
|
-
val="${val//\`/}"
|
|
78
|
-
# Remove semicolons (command chaining)
|
|
79
|
-
val="${val//;/}"
|
|
80
|
-
# Replace newlines with spaces
|
|
81
|
-
val="$(printf '%s' "$val" | tr '\n' ' ')"
|
|
82
|
-
printf '%s' "$val"
|
|
83
|
-
}
|
|
84
|
-
|
|
85
71
|
# Run bd update and check for errors in both exit code and output.
|
|
86
72
|
# bd update exits 0 even for non-existent issues, so we check stdout too.
|
|
87
73
|
bd_update() {
|
|
@@ -628,17 +614,9 @@ cmd_apply_decision() {
|
|
|
628
614
|
die "Failed to add dependency ${dependent_issue} -> ${depends_on_issue}"
|
|
629
615
|
}
|
|
630
616
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
rollback_dependency "$dependent_issue" "$depends_on_issue"
|
|
634
|
-
echo "$cycles_output" >&2
|
|
635
|
-
die "Failed to validate dependency cycles"
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
if printf '%s' "$cycles_output" | grep -Eqi 'cycle' \
|
|
639
|
-
&& ! cycles_output_is_safe "$cycles_output"; then
|
|
617
|
+
# Check for cycles — use exit code as primary signal
|
|
618
|
+
if ! ${BD_CMD:-bd} dep cycles &>/dev/null; then
|
|
640
619
|
rollback_dependency "$dependent_issue" "$depends_on_issue"
|
|
641
|
-
echo "$cycles_output" >&2
|
|
642
620
|
die "Cycle detected for ${dependent_issue} -> ${depends_on_issue}"
|
|
643
621
|
fi
|
|
644
622
|
|