aiblueprint-cli 1.4.99 → 1.4.100
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/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/prompt-creator/agents/openai.yaml +7 -0
- package/agents-config/skills/prompt-creator/assets/codex-icon.svg +16 -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/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
|
@@ -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}"
|