aiblueprint-cli 1.4.99 → 1.4.101
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 +0 -1
- package/agents-config/skills/agents-manager/SKILL.md +2 -2
- package/agents-config/skills/agents-manager/agents/openai.yaml +7 -0
- package/agents-config/skills/agents-manager/assets/codex-icon.svg +20 -0
- package/agents-config/skills/apex/SKILL.md +120 -118
- package/agents-config/skills/apex/agents/openai.yaml +10 -0
- package/agents-config/skills/apex/assets/codex-icon.svg +15 -0
- package/agents-config/skills/apex/scripts/apex-state.py +740 -0
- package/agents-config/skills/apex/scripts/setup-templates.sh +27 -145
- package/agents-config/skills/apex/scripts/test_apex_state.py +413 -0
- package/agents-config/skills/apex/scripts/update-progress.sh +17 -73
- package/agents-config/skills/apex/steps/step-00-init.md +85 -231
- package/agents-config/skills/apex/steps/step-00b-branch.md +10 -118
- package/agents-config/skills/apex/steps/step-00b-economy.md +12 -239
- package/agents-config/skills/apex/steps/step-00b-interactive.md +13 -162
- package/agents-config/skills/apex/steps/step-00b-save.md +13 -114
- package/agents-config/skills/apex/steps/step-01-analyze.md +40 -361
- package/agents-config/skills/apex/steps/step-02-plan.md +55 -562
- package/agents-config/skills/apex/steps/step-02b-tasks.md +15 -291
- package/agents-config/skills/apex/steps/step-03-execute-teams.md +47 -267
- package/agents-config/skills/apex/steps/step-03-execute.md +32 -212
- package/agents-config/skills/apex/steps/step-04-validate.md +42 -246
- package/agents-config/skills/apex/steps/step-05-examine.md +47 -371
- package/agents-config/skills/apex/steps/step-06-resolve.md +19 -221
- package/agents-config/skills/apex/steps/step-07-tests.md +19 -234
- package/agents-config/skills/apex/steps/step-08-run-tests.md +13 -300
- package/agents-config/skills/apex/steps/step-09-finish.md +36 -200
- package/agents-config/skills/apex/steps/step-10-verify.md +46 -264
- package/agents-config/skills/appstore-connect/agents/openai.yaml +7 -0
- package/agents-config/skills/appstore-connect/assets/codex-icon.svg +17 -0
- package/agents-config/skills/commit/agents/openai.yaml +10 -0
- package/agents-config/skills/commit/assets/codex-icon.svg +17 -0
- package/agents-config/skills/create-pr/agents/openai.yaml +10 -0
- package/agents-config/skills/create-pr/assets/codex-icon.svg +17 -0
- package/agents-config/skills/environments-manager/SKILL.md +1 -1
- package/agents-config/skills/environments-manager/agents/openai.yaml +7 -0
- package/agents-config/skills/environments-manager/assets/codex-icon.svg +16 -0
- package/agents-config/skills/environments-manager/examples/scripts/claude-worktree-remove.sh +19 -3
- package/agents-config/skills/environments-manager/examples/scripts/worktree-up.sh +1 -1
- package/agents-config/skills/environments-manager/references/claude.md +1 -1
- package/agents-config/skills/fix-pr-comments/agents/openai.yaml +10 -0
- package/agents-config/skills/fix-pr-comments/assets/codex-icon.svg +17 -0
- package/agents-config/skills/grill-me/SKILL.md +25 -4
- package/agents-config/skills/grill-me/agents/openai.yaml +8 -0
- package/agents-config/skills/grill-me/assets/codex-icon.svg +16 -0
- package/agents-config/skills/hooks-manager/SKILL.md +19 -9
- package/agents-config/skills/hooks-manager/assets/codex-icon.svg +15 -4
- package/agents-config/skills/hooks-manager/references/claude-code.md +32 -0
- package/agents-config/skills/hooks-manager/references/codex.md +23 -0
- package/agents-config/skills/hooks-manager/references/cursor.md +18 -0
- package/agents-config/skills/hooks-manager/references/hook-types.md +5 -3
- package/agents-config/skills/hooks-manager/references/input-output-schemas.md +2 -2
- package/agents-config/skills/hooks-manager/references/research-sources.md +25 -0
- package/agents-config/skills/hooks-manager/references/router.md +32 -0
- package/agents-config/skills/hooks-manager/references/troubleshooting.md +3 -3
- package/agents-config/skills/merge/agents/openai.yaml +10 -0
- package/agents-config/skills/merge/assets/codex-icon.svg +17 -0
- package/agents-config/skills/oneshot/SKILL.md +4 -0
- package/agents-config/skills/oneshot/agents/openai.yaml +10 -0
- package/agents-config/skills/oneshot/assets/codex-icon.svg +18 -0
- package/agents-config/skills/rules-manager/agents/openai.yaml +7 -0
- package/agents-config/skills/rules-manager/assets/codex-icon.svg +23 -0
- package/agents-config/skills/skill-manager/SKILL.md +45 -3
- package/agents-config/skills/skill-manager/agents/openai.yaml +7 -0
- package/agents-config/skills/skill-manager/assets/codex-icon.svg +23 -0
- package/agents-config/skills/skill-manager/references/skill-writing-glossary.md +201 -0
- package/agents-config/skills/skill-manager/scripts/setup-codex-icons.ts +143 -0
- package/agents-config/skills/ultrathink/agents/openai.yaml +10 -0
- package/agents-config/skills/ultrathink/assets/codex-icon.svg +20 -0
- package/agents-config/skills/use-artifacts/SKILL.md +102 -51
- package/agents-config/skills/use-artifacts/assets/local-runtime.js +299 -0
- package/agents-config/skills/use-artifacts/scripts/create_artifact.py +1 -1
- package/agents-config/skills/use-delegate/SKILL.md +4 -0
- package/agents-config/skills/use-delegate/agents/openai.yaml +10 -0
- package/agents-config/skills/use-delegate/assets/codex-icon.svg +20 -0
- package/agents-config/skills/use-goal/SKILL.md +70 -9
- package/agents-config/skills/use-goal/agents/openai.yaml +1 -1
- package/agents-config/skills/use-goal/assets/codex-icon.svg +17 -3
- package/agents-config/skills/use-goal/references/claude-code-goal.md +54 -6
- package/agents-config/skills/use-goal/references/codex-goal.md +59 -4
- package/agents-config/skills/use-goal/references/verification-harnesses.md +104 -3
- package/dist/cli.js +365 -363
- package/package.json +1 -1
- package/agents-config/skills/apex/templates/00-context.md +0 -55
- package/agents-config/skills/apex/templates/01-analyze.md +0 -10
- package/agents-config/skills/apex/templates/02-plan.md +0 -10
- package/agents-config/skills/apex/templates/03-execute.md +0 -10
- package/agents-config/skills/apex/templates/04-validate.md +0 -10
- package/agents-config/skills/apex/templates/05-examine.md +0 -10
- package/agents-config/skills/apex/templates/06-resolve.md +0 -10
- package/agents-config/skills/apex/templates/07-tests.md +0 -10
- package/agents-config/skills/apex/templates/08-run-tests.md +0 -10
- package/agents-config/skills/apex/templates/09-finish.md +0 -10
- package/agents-config/skills/apex/templates/10-verify.md +0 -9
- package/agents-config/skills/apex/templates/README.md +0 -195
- package/agents-config/skills/apex/templates/step-complete.md +0 -7
- package/agents-config/skills/prompt-creator/SKILL.md +0 -285
- package/agents-config/skills/prompt-creator/references/anthropic-best-practices.md +0 -126
- package/agents-config/skills/prompt-creator/references/anti-patterns.md +0 -57
- package/agents-config/skills/prompt-creator/references/clarity-principles.md +0 -54
- package/agents-config/skills/prompt-creator/references/context-management.md +0 -389
- package/agents-config/skills/prompt-creator/references/few-shot-patterns.md +0 -47
- package/agents-config/skills/prompt-creator/references/openai-best-practices.md +0 -50
- package/agents-config/skills/prompt-creator/references/prompt-templates.md +0 -110
- package/agents-config/skills/prompt-creator/references/reasoning-techniques.md +0 -52
- package/agents-config/skills/prompt-creator/references/system-prompt-patterns.md +0 -48
- package/agents-config/skills/prompt-creator/references/xml-structure.md +0 -36
|
@@ -1,154 +1,36 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
#
|
|
3
|
-
# Creates output directory structure and initializes template files
|
|
4
|
-
#
|
|
5
|
-
# Usage: setup-templates.sh "feature-name" [other args...]
|
|
6
|
-
# The script auto-generates the task ID with the next available number.
|
|
2
|
+
# Compatibility initializer for older APEX callers.
|
|
7
3
|
|
|
8
|
-
set -
|
|
4
|
+
set -euo pipefail
|
|
9
5
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
TASK_DESCRIPTION="$2"
|
|
13
|
-
AUTO_MODE="${3:-false}"
|
|
14
|
-
EXAMINE_MODE="${4:-false}"
|
|
15
|
-
SAVE_MODE="${5:-false}"
|
|
16
|
-
TEST_MODE="${6:-false}"
|
|
17
|
-
ECONOMY_MODE="${7:-false}"
|
|
18
|
-
BRANCH_MODE="${8:-false}"
|
|
19
|
-
PR_MODE="${9:-false}"
|
|
20
|
-
INTERACTIVE_MODE="${10:-false}"
|
|
21
|
-
TASKS_MODE="${11:-false}"
|
|
22
|
-
VERIFY_MODE="${12:-false}"
|
|
23
|
-
BRANCH_NAME="${13:-}"
|
|
24
|
-
ORIGINAL_INPUT="${14:-}"
|
|
6
|
+
FEATURE_NAME="${1:-}"
|
|
7
|
+
TASK_DESCRIPTION="${2:-}"
|
|
25
8
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
exit 1
|
|
30
|
-
fi
|
|
31
|
-
|
|
32
|
-
if [[ -z "$TASK_DESCRIPTION" ]]; then
|
|
33
|
-
echo "Error: TASK_DESCRIPTION is required"
|
|
34
|
-
exit 1
|
|
35
|
-
fi
|
|
36
|
-
|
|
37
|
-
# Get current timestamp
|
|
38
|
-
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
39
|
-
|
|
40
|
-
# Use current working directory as project root
|
|
41
|
-
PROJECT_ROOT=$(pwd)
|
|
42
|
-
APEX_OUTPUT_DIR="${PROJECT_ROOT}/.claude/output/apex"
|
|
43
|
-
|
|
44
|
-
# Create apex output directory if it doesn't exist
|
|
45
|
-
mkdir -p "$APEX_OUTPUT_DIR"
|
|
46
|
-
|
|
47
|
-
# Find the next available number
|
|
48
|
-
NEXT_NUM=1
|
|
49
|
-
if [[ -d "$APEX_OUTPUT_DIR" ]]; then
|
|
50
|
-
# Find highest existing number prefix
|
|
51
|
-
HIGHEST=$(ls -1 "$APEX_OUTPUT_DIR" 2>/dev/null | grep -oE '^[0-9]+' | sort -n | tail -1)
|
|
52
|
-
if [[ -n "$HIGHEST" ]]; then
|
|
53
|
-
# Force base-10 interpretation (leading zeros would be treated as octal)
|
|
54
|
-
NEXT_NUM=$((10#$HIGHEST + 1))
|
|
55
|
-
fi
|
|
56
|
-
fi
|
|
57
|
-
|
|
58
|
-
# Format with leading zeros (2 digits)
|
|
59
|
-
TASK_NUM=$(printf "%02d" "$NEXT_NUM")
|
|
60
|
-
|
|
61
|
-
# Build full task ID
|
|
62
|
-
TASK_ID="${TASK_NUM}-${FEATURE_NAME}"
|
|
63
|
-
|
|
64
|
-
OUTPUT_DIR="${APEX_OUTPUT_DIR}/${TASK_ID}"
|
|
65
|
-
|
|
66
|
-
# Get skill directory
|
|
67
|
-
SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
68
|
-
TEMPLATE_DIR="${SKILL_DIR}/templates"
|
|
69
|
-
|
|
70
|
-
# Create output directory
|
|
71
|
-
mkdir -p "$OUTPUT_DIR"
|
|
72
|
-
|
|
73
|
-
# Function to replace template variables
|
|
74
|
-
render_template() {
|
|
75
|
-
local template_file="$1"
|
|
76
|
-
local output_file="$2"
|
|
77
|
-
|
|
78
|
-
# Determine status strings based on flags
|
|
79
|
-
local examine_status="⏭ Skip"
|
|
80
|
-
[[ "$EXAMINE_MODE" == "true" ]] && examine_status="⏸ Pending"
|
|
81
|
-
|
|
82
|
-
local test_status="⏭ Skip"
|
|
83
|
-
[[ "$TEST_MODE" == "true" ]] && test_status="⏸ Pending"
|
|
84
|
-
|
|
85
|
-
local pr_status="⏭ Skip"
|
|
86
|
-
[[ "$PR_MODE" == "true" ]] && pr_status="⏸ Pending"
|
|
87
|
-
|
|
88
|
-
local verify_status="⏭ Skip"
|
|
89
|
-
[[ "$VERIFY_MODE" == "true" ]] && verify_status="⏸ Pending"
|
|
90
|
-
|
|
91
|
-
local tasks_status="⏭ Skip"
|
|
92
|
-
[[ "$TASKS_MODE" == "true" ]] && tasks_status="⏸ Pending"
|
|
93
|
-
|
|
94
|
-
# Read template and replace variables
|
|
95
|
-
sed -e "s|{{task_id}}|${TASK_ID}|g" \
|
|
96
|
-
-e "s|{{task_description}}|${TASK_DESCRIPTION}|g" \
|
|
97
|
-
-e "s|{{timestamp}}|${TIMESTAMP}|g" \
|
|
98
|
-
-e "s|{{auto_mode}}|${AUTO_MODE}|g" \
|
|
99
|
-
-e "s|{{examine_mode}}|${EXAMINE_MODE}|g" \
|
|
100
|
-
-e "s|{{save_mode}}|${SAVE_MODE}|g" \
|
|
101
|
-
-e "s|{{test_mode}}|${TEST_MODE}|g" \
|
|
102
|
-
-e "s|{{economy_mode}}|${ECONOMY_MODE}|g" \
|
|
103
|
-
-e "s|{{branch_mode}}|${BRANCH_MODE}|g" \
|
|
104
|
-
-e "s|{{pr_mode}}|${PR_MODE}|g" \
|
|
105
|
-
-e "s|{{interactive_mode}}|${INTERACTIVE_MODE}|g" \
|
|
106
|
-
-e "s|{{tasks_mode}}|${TASKS_MODE}|g" \
|
|
107
|
-
-e "s|{{verify_mode}}|${VERIFY_MODE}|g" \
|
|
108
|
-
-e "s|{{branch_name}}|${BRANCH_NAME}|g" \
|
|
109
|
-
-e "s|{{original_input}}|${ORIGINAL_INPUT}|g" \
|
|
110
|
-
-e "s|{{examine_status}}|${examine_status}|g" \
|
|
111
|
-
-e "s|{{test_status}}|${test_status}|g" \
|
|
112
|
-
-e "s|{{tasks_status}}|${tasks_status}|g" \
|
|
113
|
-
-e "s|{{verify_status}}|${verify_status}|g" \
|
|
114
|
-
-e "s|{{pr_status}}|${pr_status}|g" \
|
|
115
|
-
"$template_file" > "$output_file"
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
# Initialize 00-context.md
|
|
119
|
-
render_template "${TEMPLATE_DIR}/00-context.md" "${OUTPUT_DIR}/00-context.md"
|
|
120
|
-
|
|
121
|
-
# Initialize other step files (only headers, content appended during execution)
|
|
122
|
-
render_template "${TEMPLATE_DIR}/01-analyze.md" "${OUTPUT_DIR}/01-analyze.md"
|
|
123
|
-
render_template "${TEMPLATE_DIR}/02-plan.md" "${OUTPUT_DIR}/02-plan.md"
|
|
124
|
-
render_template "${TEMPLATE_DIR}/03-execute.md" "${OUTPUT_DIR}/03-execute.md"
|
|
125
|
-
render_template "${TEMPLATE_DIR}/04-validate.md" "${OUTPUT_DIR}/04-validate.md"
|
|
126
|
-
|
|
127
|
-
# Conditional templates
|
|
128
|
-
if [[ "$EXAMINE_MODE" == "true" ]]; then
|
|
129
|
-
render_template "${TEMPLATE_DIR}/05-examine.md" "${OUTPUT_DIR}/05-examine.md"
|
|
130
|
-
render_template "${TEMPLATE_DIR}/06-resolve.md" "${OUTPUT_DIR}/06-resolve.md"
|
|
131
|
-
fi
|
|
132
|
-
|
|
133
|
-
if [[ "$TEST_MODE" == "true" ]]; then
|
|
134
|
-
render_template "${TEMPLATE_DIR}/07-tests.md" "${OUTPUT_DIR}/07-tests.md"
|
|
135
|
-
render_template "${TEMPLATE_DIR}/08-run-tests.md" "${OUTPUT_DIR}/08-run-tests.md"
|
|
136
|
-
fi
|
|
137
|
-
|
|
138
|
-
if [[ "$VERIFY_MODE" == "true" ]]; then
|
|
139
|
-
render_template "${TEMPLATE_DIR}/10-verify.md" "${OUTPUT_DIR}/10-verify.md"
|
|
9
|
+
if [[ -z "$FEATURE_NAME" || -z "$TASK_DESCRIPTION" ]]; then
|
|
10
|
+
echo "Usage: $0 <feature-name> <task-description> [legacy-options...]" >&2
|
|
11
|
+
exit 1
|
|
140
12
|
fi
|
|
141
13
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
fi
|
|
14
|
+
PROJECT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
|
|
15
|
+
SKILL_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
|
145
16
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
17
|
+
NEXT_NUMBER=1
|
|
18
|
+
while true; do
|
|
19
|
+
RUN_ID=$(printf "%02d-%s" "$NEXT_NUMBER" "$FEATURE_NAME")
|
|
20
|
+
if RESULT=$(python3 "${SKILL_DIR}/scripts/apex-state.py" init \
|
|
21
|
+
--root "$PROJECT_ROOT" \
|
|
22
|
+
--task "$TASK_DESCRIPTION" \
|
|
23
|
+
--run-id "$RUN_ID" 2>&1); then
|
|
24
|
+
break
|
|
25
|
+
fi
|
|
26
|
+
if [[ "$RESULT" != *"run already exists"* ]]; then
|
|
27
|
+
echo "$RESULT" >&2
|
|
28
|
+
exit 1
|
|
29
|
+
fi
|
|
30
|
+
NEXT_NUMBER=$((NEXT_NUMBER + 1))
|
|
31
|
+
done
|
|
149
32
|
|
|
150
|
-
|
|
151
|
-
echo "TASK_ID=${
|
|
33
|
+
OUTPUT_DIR=$(python3 -c 'import json,sys; print(json.load(sys.stdin)["run_dir"])' <<<"$RESULT")
|
|
34
|
+
echo "TASK_ID=${RUN_ID}"
|
|
152
35
|
echo "OUTPUT_DIR=${OUTPUT_DIR}"
|
|
153
|
-
echo "
|
|
154
|
-
exit 0
|
|
36
|
+
echo "APEX run initialized: ${OUTPUT_DIR}"
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import stat
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import tempfile
|
|
10
|
+
import unittest
|
|
11
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
SCRIPT = Path(__file__).with_name("apex-state.py")
|
|
16
|
+
SETUP_SCRIPT = Path(__file__).with_name("setup-templates.sh")
|
|
17
|
+
UPDATE_SCRIPT = Path(__file__).with_name("update-progress.sh")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ApexStateTest(unittest.TestCase):
|
|
21
|
+
def setUp(self) -> None:
|
|
22
|
+
self.temporary = tempfile.TemporaryDirectory()
|
|
23
|
+
self.root = Path(self.temporary.name)
|
|
24
|
+
|
|
25
|
+
def tearDown(self) -> None:
|
|
26
|
+
self.temporary.cleanup()
|
|
27
|
+
|
|
28
|
+
def run_cli(self, *arguments: str, check: bool = True) -> subprocess.CompletedProcess[str]:
|
|
29
|
+
return subprocess.run(
|
|
30
|
+
[sys.executable, str(SCRIPT), *arguments],
|
|
31
|
+
check=check,
|
|
32
|
+
capture_output=True,
|
|
33
|
+
text=True,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
def initialize(self, task: str = "Test durable state") -> dict:
|
|
37
|
+
result = self.run_cli(
|
|
38
|
+
"init",
|
|
39
|
+
"--root",
|
|
40
|
+
str(self.root),
|
|
41
|
+
"--task",
|
|
42
|
+
task,
|
|
43
|
+
"--run-id",
|
|
44
|
+
"test-run",
|
|
45
|
+
)
|
|
46
|
+
return json.loads(result.stdout)
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def directory(self) -> Path:
|
|
50
|
+
return self.root / ".agents/apex/runs/test-run"
|
|
51
|
+
|
|
52
|
+
def test_full_artifact_lifecycle_and_journal_recovery(self) -> None:
|
|
53
|
+
initialized = self.initialize()
|
|
54
|
+
self.assertEqual(initialized["run_id"], "test-run")
|
|
55
|
+
self.assertNotIn("state", initialized)
|
|
56
|
+
|
|
57
|
+
self.run_cli(
|
|
58
|
+
"event",
|
|
59
|
+
"--root",
|
|
60
|
+
str(self.root),
|
|
61
|
+
"--run-id",
|
|
62
|
+
"test-run",
|
|
63
|
+
"--phase",
|
|
64
|
+
"analyze",
|
|
65
|
+
"--status",
|
|
66
|
+
"complete",
|
|
67
|
+
"--message",
|
|
68
|
+
"Analysis complete",
|
|
69
|
+
)
|
|
70
|
+
self.run_cli(
|
|
71
|
+
"checkpoint",
|
|
72
|
+
"--root",
|
|
73
|
+
str(self.root),
|
|
74
|
+
"--run-id",
|
|
75
|
+
"test-run",
|
|
76
|
+
"--phase",
|
|
77
|
+
"plan",
|
|
78
|
+
"--message",
|
|
79
|
+
"Plan ready",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
artifact = json.loads(
|
|
83
|
+
self.run_cli(
|
|
84
|
+
"artifact",
|
|
85
|
+
"--root",
|
|
86
|
+
str(self.root),
|
|
87
|
+
"--run-id",
|
|
88
|
+
"test-run",
|
|
89
|
+
"--path",
|
|
90
|
+
str(self.directory / "tasks.json"),
|
|
91
|
+
"--type",
|
|
92
|
+
"task-graph",
|
|
93
|
+
"--phase",
|
|
94
|
+
"plan",
|
|
95
|
+
).stdout
|
|
96
|
+
)
|
|
97
|
+
self.assertEqual(artifact["status"], "current")
|
|
98
|
+
|
|
99
|
+
invalidated = json.loads(
|
|
100
|
+
self.run_cli(
|
|
101
|
+
"invalidate",
|
|
102
|
+
"--root",
|
|
103
|
+
str(self.root),
|
|
104
|
+
"--run-id",
|
|
105
|
+
"test-run",
|
|
106
|
+
"--artifact-id",
|
|
107
|
+
artifact["id"],
|
|
108
|
+
"--message",
|
|
109
|
+
"Plan changed",
|
|
110
|
+
).stdout
|
|
111
|
+
)
|
|
112
|
+
self.assertEqual(invalidated["status"], "invalidated")
|
|
113
|
+
|
|
114
|
+
# Corrupt projections, then prove status recovers from the journal.
|
|
115
|
+
projected = json.loads((self.directory / "run.json").read_text())
|
|
116
|
+
projected["last_event_id"] = "stale"
|
|
117
|
+
(self.directory / "run.json").write_text(json.dumps(projected))
|
|
118
|
+
(self.directory / "evidence.json").write_text('{"schema_version": 1, "artifacts": []}')
|
|
119
|
+
|
|
120
|
+
status = json.loads(
|
|
121
|
+
self.run_cli("status", "--root", str(self.root), "--run-id", "test-run").stdout
|
|
122
|
+
)
|
|
123
|
+
self.assertNotEqual(status["state"]["last_event_id"], "stale")
|
|
124
|
+
self.assertEqual(status["evidence"]["artifacts"][0]["status"], "invalidated")
|
|
125
|
+
self.assertFalse(status["drift"]["known"])
|
|
126
|
+
|
|
127
|
+
def test_concurrent_events_keep_unique_ids(self) -> None:
|
|
128
|
+
self.initialize()
|
|
129
|
+
|
|
130
|
+
def record(number: int) -> None:
|
|
131
|
+
self.run_cli(
|
|
132
|
+
"event",
|
|
133
|
+
"--root",
|
|
134
|
+
str(self.root),
|
|
135
|
+
"--run-id",
|
|
136
|
+
"test-run",
|
|
137
|
+
"--phase",
|
|
138
|
+
"execute",
|
|
139
|
+
"--status",
|
|
140
|
+
"complete",
|
|
141
|
+
"--message",
|
|
142
|
+
f"Event {number}",
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
with ThreadPoolExecutor(max_workers=6) as executor:
|
|
146
|
+
list(executor.map(record, range(12)))
|
|
147
|
+
|
|
148
|
+
events = [json.loads(line) for line in (self.directory / "journal.jsonl").read_text().splitlines()]
|
|
149
|
+
ids = [event["id"] for event in events]
|
|
150
|
+
self.assertEqual(len(ids), 13)
|
|
151
|
+
self.assertEqual(len(set(ids)), 13)
|
|
152
|
+
|
|
153
|
+
def test_concurrent_init_has_one_winner(self) -> None:
|
|
154
|
+
def initialize(_: int) -> int:
|
|
155
|
+
return self.run_cli(
|
|
156
|
+
"init",
|
|
157
|
+
"--root",
|
|
158
|
+
str(self.root),
|
|
159
|
+
"--task",
|
|
160
|
+
"Concurrent",
|
|
161
|
+
"--run-id",
|
|
162
|
+
"same-run",
|
|
163
|
+
check=False,
|
|
164
|
+
).returncode
|
|
165
|
+
|
|
166
|
+
with ThreadPoolExecutor(max_workers=2) as executor:
|
|
167
|
+
results = list(executor.map(initialize, range(2)))
|
|
168
|
+
self.assertEqual(sorted(results), [0, 1])
|
|
169
|
+
self.assertEqual(list((self.root / ".agents/apex/runs").glob(".initializing-*")), [])
|
|
170
|
+
|
|
171
|
+
def test_concurrent_distinct_init_on_fresh_root(self) -> None:
|
|
172
|
+
def initialize(number: int) -> int:
|
|
173
|
+
return self.run_cli(
|
|
174
|
+
"init",
|
|
175
|
+
"--root",
|
|
176
|
+
str(self.root),
|
|
177
|
+
"--task",
|
|
178
|
+
f"Concurrent {number}",
|
|
179
|
+
"--run-id",
|
|
180
|
+
f"run-{number}",
|
|
181
|
+
check=False,
|
|
182
|
+
).returncode
|
|
183
|
+
|
|
184
|
+
with ThreadPoolExecutor(max_workers=2) as executor:
|
|
185
|
+
results = list(executor.map(initialize, range(2)))
|
|
186
|
+
self.assertEqual(results, [0, 0])
|
|
187
|
+
|
|
188
|
+
def test_rejects_symlinked_state_root(self) -> None:
|
|
189
|
+
outside = self.root / "outside"
|
|
190
|
+
outside.mkdir()
|
|
191
|
+
(self.root / ".agents").symlink_to(outside, target_is_directory=True)
|
|
192
|
+
result = self.run_cli(
|
|
193
|
+
"init",
|
|
194
|
+
"--root",
|
|
195
|
+
str(self.root),
|
|
196
|
+
"--task",
|
|
197
|
+
"Symlink",
|
|
198
|
+
"--run-id",
|
|
199
|
+
"test-run",
|
|
200
|
+
check=False,
|
|
201
|
+
)
|
|
202
|
+
self.assertNotEqual(result.returncode, 0)
|
|
203
|
+
self.assertIn("symlink", result.stderr)
|
|
204
|
+
|
|
205
|
+
def test_redacts_secrets_and_uses_private_modes(self) -> None:
|
|
206
|
+
secret = "sk-abcdefghijklmnopqrstuv"
|
|
207
|
+
self.initialize(f"Fix auth token={secret}")
|
|
208
|
+
state_text = (self.directory / "run.json").read_text()
|
|
209
|
+
self.assertNotIn(secret, state_text)
|
|
210
|
+
self.assertIn("[REDACTED_SECRET]", state_text)
|
|
211
|
+
self.assertEqual(stat.S_IMODE(self.directory.stat().st_mode), 0o700)
|
|
212
|
+
for name in ("run.json", "tasks.json", "evidence.json", "journal.jsonl"):
|
|
213
|
+
self.assertEqual(stat.S_IMODE((self.directory / name).stat().st_mode), 0o600)
|
|
214
|
+
|
|
215
|
+
def test_rejects_secrets_in_metadata(self) -> None:
|
|
216
|
+
self.initialize()
|
|
217
|
+
secret = "sk-abcdefghijklmnopqrstuv"
|
|
218
|
+
for field, value in (("--task-id", secret), ("--phase", secret)):
|
|
219
|
+
arguments = [
|
|
220
|
+
"event",
|
|
221
|
+
"--root",
|
|
222
|
+
str(self.root),
|
|
223
|
+
"--run-id",
|
|
224
|
+
"test-run",
|
|
225
|
+
"--phase",
|
|
226
|
+
"execute",
|
|
227
|
+
"--status",
|
|
228
|
+
"complete",
|
|
229
|
+
"--message",
|
|
230
|
+
"Safe",
|
|
231
|
+
]
|
|
232
|
+
index = arguments.index(field) if field in arguments else None
|
|
233
|
+
if index is None:
|
|
234
|
+
arguments.extend((field, value))
|
|
235
|
+
else:
|
|
236
|
+
arguments[index + 1] = value
|
|
237
|
+
result = self.run_cli(*arguments, check=False)
|
|
238
|
+
self.assertNotEqual(result.returncode, 0)
|
|
239
|
+
self.assertNotIn(secret, result.stderr)
|
|
240
|
+
|
|
241
|
+
artifact_path = self.directory / f"token={secret}"
|
|
242
|
+
artifact_path.write_text("sensitive name")
|
|
243
|
+
result = self.run_cli(
|
|
244
|
+
"artifact",
|
|
245
|
+
"--root",
|
|
246
|
+
str(self.root),
|
|
247
|
+
"--run-id",
|
|
248
|
+
"test-run",
|
|
249
|
+
"--path",
|
|
250
|
+
str(artifact_path),
|
|
251
|
+
"--type",
|
|
252
|
+
"task-graph",
|
|
253
|
+
"--phase",
|
|
254
|
+
"plan",
|
|
255
|
+
check=False,
|
|
256
|
+
)
|
|
257
|
+
self.assertNotEqual(result.returncode, 0)
|
|
258
|
+
self.assertNotIn(secret, result.stderr)
|
|
259
|
+
|
|
260
|
+
def test_tampered_artifact_is_reported_stale(self) -> None:
|
|
261
|
+
self.initialize()
|
|
262
|
+
artifact_path = self.directory / "tasks.json"
|
|
263
|
+
artifact = json.loads(
|
|
264
|
+
self.run_cli(
|
|
265
|
+
"artifact",
|
|
266
|
+
"--root",
|
|
267
|
+
str(self.root),
|
|
268
|
+
"--run-id",
|
|
269
|
+
"test-run",
|
|
270
|
+
"--path",
|
|
271
|
+
str(artifact_path),
|
|
272
|
+
"--type",
|
|
273
|
+
"task-graph",
|
|
274
|
+
"--phase",
|
|
275
|
+
"plan",
|
|
276
|
+
).stdout
|
|
277
|
+
)
|
|
278
|
+
artifact_path.write_text('{"tampered": true}')
|
|
279
|
+
status = json.loads(
|
|
280
|
+
self.run_cli("status", "--root", str(self.root), "--run-id", "test-run").stdout
|
|
281
|
+
)
|
|
282
|
+
current = next(item for item in status["evidence"]["artifacts"] if item["id"] == artifact["id"])
|
|
283
|
+
self.assertEqual(current["status"], "stale")
|
|
284
|
+
self.assertEqual(current["integrity"], "mismatch")
|
|
285
|
+
|
|
286
|
+
def test_unborn_git_revision_makes_drift_unknown(self) -> None:
|
|
287
|
+
subprocess.run(["git", "init", "-q", str(self.root)], check=True)
|
|
288
|
+
self.initialize()
|
|
289
|
+
status = json.loads(
|
|
290
|
+
self.run_cli("status", "--root", str(self.root), "--run-id", "test-run").stdout
|
|
291
|
+
)
|
|
292
|
+
self.assertFalse(status["state"]["baseline"]["verified"])
|
|
293
|
+
self.assertFalse(status["drift"]["known"])
|
|
294
|
+
|
|
295
|
+
def test_redacted_git_paths_still_produce_known_drift(self) -> None:
|
|
296
|
+
subprocess.run(["git", "init", "-q", str(self.root)], check=True)
|
|
297
|
+
subprocess.run(["git", "-C", str(self.root), "config", "user.email", "test@example.com"], check=True)
|
|
298
|
+
subprocess.run(["git", "-C", str(self.root), "config", "user.name", "APEX Test"], check=True)
|
|
299
|
+
tracked = self.root / "tracked.txt"
|
|
300
|
+
tracked.write_text("baseline")
|
|
301
|
+
subprocess.run(["git", "-C", str(self.root), "add", "tracked.txt"], check=True)
|
|
302
|
+
subprocess.run(["git", "-C", str(self.root), "commit", "-qm", "test baseline"], check=True)
|
|
303
|
+
before = self.root / "secret=old"
|
|
304
|
+
before.write_text("before")
|
|
305
|
+
self.initialize()
|
|
306
|
+
before.unlink()
|
|
307
|
+
(self.root / "secret=new").write_text("after")
|
|
308
|
+
|
|
309
|
+
status = json.loads(
|
|
310
|
+
self.run_cli("status", "--root", str(self.root), "--run-id", "test-run").stdout
|
|
311
|
+
)
|
|
312
|
+
self.assertTrue(status["state"]["baseline"]["paths_redacted"])
|
|
313
|
+
self.assertTrue(status["drift"]["known"])
|
|
314
|
+
self.assertTrue(status["drift"]["changes_changed"])
|
|
315
|
+
|
|
316
|
+
def test_redacted_branch_names_still_produce_known_drift(self) -> None:
|
|
317
|
+
subprocess.run(["git", "init", "-q", str(self.root)], check=True)
|
|
318
|
+
subprocess.run(["git", "-C", str(self.root), "config", "user.email", "test@example.com"], check=True)
|
|
319
|
+
subprocess.run(["git", "-C", str(self.root), "config", "user.name", "APEX Test"], check=True)
|
|
320
|
+
tracked = self.root / "tracked.txt"
|
|
321
|
+
tracked.write_text("baseline")
|
|
322
|
+
subprocess.run(["git", "-C", str(self.root), "add", "tracked.txt"], check=True)
|
|
323
|
+
subprocess.run(["git", "-C", str(self.root), "commit", "-qm", "test baseline"], check=True)
|
|
324
|
+
subprocess.run(["git", "-C", str(self.root), "checkout", "-qb", "secret=old"], check=True)
|
|
325
|
+
self.initialize()
|
|
326
|
+
subprocess.run(["git", "-C", str(self.root), "checkout", "-qb", "secret=new"], check=True)
|
|
327
|
+
|
|
328
|
+
status = json.loads(
|
|
329
|
+
self.run_cli("status", "--root", str(self.root), "--run-id", "test-run").stdout
|
|
330
|
+
)
|
|
331
|
+
self.assertTrue(status["drift"]["known"])
|
|
332
|
+
self.assertTrue(status["drift"]["branch_changed"])
|
|
333
|
+
|
|
334
|
+
def test_terminal_status_survives_final_checkpoint(self) -> None:
|
|
335
|
+
self.initialize()
|
|
336
|
+
self.run_cli(
|
|
337
|
+
"event",
|
|
338
|
+
"--root",
|
|
339
|
+
str(self.root),
|
|
340
|
+
"--run-id",
|
|
341
|
+
"test-run",
|
|
342
|
+
"--phase",
|
|
343
|
+
"handoff",
|
|
344
|
+
"--status",
|
|
345
|
+
"complete",
|
|
346
|
+
"--message",
|
|
347
|
+
"Complete",
|
|
348
|
+
)
|
|
349
|
+
self.run_cli(
|
|
350
|
+
"checkpoint",
|
|
351
|
+
"--root",
|
|
352
|
+
str(self.root),
|
|
353
|
+
"--run-id",
|
|
354
|
+
"test-run",
|
|
355
|
+
"--phase",
|
|
356
|
+
"handoff",
|
|
357
|
+
"--message",
|
|
358
|
+
"Final checkpoint",
|
|
359
|
+
)
|
|
360
|
+
status = json.loads(
|
|
361
|
+
self.run_cli("status", "--root", str(self.root), "--run-id", "test-run").stdout
|
|
362
|
+
)
|
|
363
|
+
self.assertEqual(status["state"]["status"], "complete")
|
|
364
|
+
|
|
365
|
+
def test_legacy_wrappers_keep_numbered_id_and_output_contract(self) -> None:
|
|
366
|
+
first = subprocess.run(
|
|
367
|
+
["bash", str(SETUP_SCRIPT), "legacy-feature", "Legacy task"],
|
|
368
|
+
cwd=self.root,
|
|
369
|
+
check=True,
|
|
370
|
+
capture_output=True,
|
|
371
|
+
text=True,
|
|
372
|
+
)
|
|
373
|
+
values = dict(
|
|
374
|
+
line.split("=", 1)
|
|
375
|
+
for line in first.stdout.splitlines()
|
|
376
|
+
if line.startswith(("TASK_ID=", "OUTPUT_DIR="))
|
|
377
|
+
)
|
|
378
|
+
self.assertEqual(values["TASK_ID"], "01-legacy-feature")
|
|
379
|
+
self.assertTrue(Path(values["OUTPUT_DIR"]).is_dir())
|
|
380
|
+
|
|
381
|
+
subprocess.run(
|
|
382
|
+
["bash", str(UPDATE_SCRIPT), values["TASK_ID"], "01", "analyze", "complete"],
|
|
383
|
+
cwd=self.root,
|
|
384
|
+
check=True,
|
|
385
|
+
capture_output=True,
|
|
386
|
+
text=True,
|
|
387
|
+
)
|
|
388
|
+
second = subprocess.run(
|
|
389
|
+
["bash", str(SETUP_SCRIPT), "legacy-feature", "Legacy task 2"],
|
|
390
|
+
cwd=self.root,
|
|
391
|
+
check=True,
|
|
392
|
+
capture_output=True,
|
|
393
|
+
text=True,
|
|
394
|
+
)
|
|
395
|
+
self.assertIn("TASK_ID=02-legacy-feature", second.stdout)
|
|
396
|
+
|
|
397
|
+
def test_rejects_invalid_run_id(self) -> None:
|
|
398
|
+
result = self.run_cli(
|
|
399
|
+
"init",
|
|
400
|
+
"--root",
|
|
401
|
+
str(self.root),
|
|
402
|
+
"--task",
|
|
403
|
+
"Invalid",
|
|
404
|
+
"--run-id",
|
|
405
|
+
"../escape",
|
|
406
|
+
check=False,
|
|
407
|
+
)
|
|
408
|
+
self.assertNotEqual(result.returncode, 0)
|
|
409
|
+
self.assertIn("run ID", result.stderr)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
if __name__ == "__main__":
|
|
413
|
+
unittest.main()
|
|
@@ -1,80 +1,24 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
#
|
|
3
|
-
# Updates 00-context.md progress table
|
|
2
|
+
# Backward-compatible progress event writer for older APEX callers.
|
|
4
3
|
|
|
5
|
-
set -
|
|
4
|
+
set -euo pipefail
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
STATUS="$4" # "in_progress" or "complete"
|
|
6
|
+
RUN_ID="${1:-}"
|
|
7
|
+
STEP_NUMBER="${2:-}"
|
|
8
|
+
STEP_NAME="${3:-}"
|
|
9
|
+
STATUS="${4:-}"
|
|
12
10
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
echo "Example: $0 01-add-auth 01 analyze complete"
|
|
17
|
-
exit 1
|
|
11
|
+
if [[ -z "$RUN_ID" || -z "$STEP_NUMBER" || -z "$STEP_NAME" || -z "$STATUS" ]]; then
|
|
12
|
+
echo "Usage: $0 <run-id> <step-number> <step-name> <status>" >&2
|
|
13
|
+
exit 1
|
|
18
14
|
fi
|
|
19
15
|
|
|
20
|
-
# Find project root
|
|
21
16
|
PROJECT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
# Get timestamp
|
|
31
|
-
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
32
|
-
|
|
33
|
-
# Determine status symbol
|
|
34
|
-
if [[ "$STATUS" == "in_progress" ]]; then
|
|
35
|
-
STATUS_SYMBOL="⏳ In Progress"
|
|
36
|
-
elif [[ "$STATUS" == "complete" ]]; then
|
|
37
|
-
STATUS_SYMBOL="✓ Complete"
|
|
38
|
-
else
|
|
39
|
-
echo "Error: Invalid status. Use 'in_progress' or 'complete'"
|
|
40
|
-
exit 1
|
|
41
|
-
fi
|
|
42
|
-
|
|
43
|
-
# Create temp file
|
|
44
|
-
TEMP_FILE=$(mktemp)
|
|
45
|
-
|
|
46
|
-
# Update the progress table
|
|
47
|
-
awk -v step="${STEP_NUMBER}-${STEP_NAME}" \
|
|
48
|
-
-v status="$STATUS_SYMBOL" \
|
|
49
|
-
-v timestamp="$TIMESTAMP" '
|
|
50
|
-
BEGIN { in_table = 0; found = 0 }
|
|
51
|
-
{
|
|
52
|
-
# Detect progress table start
|
|
53
|
-
if ($0 ~ /^## Progress/) {
|
|
54
|
-
in_table = 1
|
|
55
|
-
print $0
|
|
56
|
-
next
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
# If in table and found the matching step
|
|
60
|
-
if (in_table && $0 ~ "\\| " step " \\|") {
|
|
61
|
-
printf "| %s | %s | %s |\n", step, status, timestamp
|
|
62
|
-
found = 1
|
|
63
|
-
next
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
# Print line as-is
|
|
67
|
-
print $0
|
|
68
|
-
}
|
|
69
|
-
END {
|
|
70
|
-
if (!found) {
|
|
71
|
-
print "Warning: Step not found in progress table" > "/dev/stderr"
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
' "$CONTEXT_FILE" > "$TEMP_FILE"
|
|
75
|
-
|
|
76
|
-
# Replace original file
|
|
77
|
-
mv "$TEMP_FILE" "$CONTEXT_FILE"
|
|
78
|
-
|
|
79
|
-
echo "✓ Progress updated: ${STEP_NUMBER}-${STEP_NAME} → ${STATUS_SYMBOL}"
|
|
80
|
-
exit 0
|
|
17
|
+
SKILL_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
|
18
|
+
|
|
19
|
+
python3 "${SKILL_DIR}/scripts/apex-state.py" event \
|
|
20
|
+
--root "$PROJECT_ROOT" \
|
|
21
|
+
--run-id "$RUN_ID" \
|
|
22
|
+
--phase "$STEP_NAME" \
|
|
23
|
+
--status "$STATUS" \
|
|
24
|
+
--message "Legacy progress update ${STEP_NUMBER}-${STEP_NAME}"
|