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.
Files changed (107) hide show
  1. package/README.md +0 -1
  2. package/agents-config/skills/agents-manager/SKILL.md +2 -2
  3. package/agents-config/skills/agents-manager/agents/openai.yaml +7 -0
  4. package/agents-config/skills/agents-manager/assets/codex-icon.svg +20 -0
  5. package/agents-config/skills/apex/SKILL.md +120 -118
  6. package/agents-config/skills/apex/agents/openai.yaml +10 -0
  7. package/agents-config/skills/apex/assets/codex-icon.svg +15 -0
  8. package/agents-config/skills/apex/scripts/apex-state.py +740 -0
  9. package/agents-config/skills/apex/scripts/setup-templates.sh +27 -145
  10. package/agents-config/skills/apex/scripts/test_apex_state.py +413 -0
  11. package/agents-config/skills/apex/scripts/update-progress.sh +17 -73
  12. package/agents-config/skills/apex/steps/step-00-init.md +85 -231
  13. package/agents-config/skills/apex/steps/step-00b-branch.md +10 -118
  14. package/agents-config/skills/apex/steps/step-00b-economy.md +12 -239
  15. package/agents-config/skills/apex/steps/step-00b-interactive.md +13 -162
  16. package/agents-config/skills/apex/steps/step-00b-save.md +13 -114
  17. package/agents-config/skills/apex/steps/step-01-analyze.md +40 -361
  18. package/agents-config/skills/apex/steps/step-02-plan.md +55 -562
  19. package/agents-config/skills/apex/steps/step-02b-tasks.md +15 -291
  20. package/agents-config/skills/apex/steps/step-03-execute-teams.md +47 -267
  21. package/agents-config/skills/apex/steps/step-03-execute.md +32 -212
  22. package/agents-config/skills/apex/steps/step-04-validate.md +42 -246
  23. package/agents-config/skills/apex/steps/step-05-examine.md +47 -371
  24. package/agents-config/skills/apex/steps/step-06-resolve.md +19 -221
  25. package/agents-config/skills/apex/steps/step-07-tests.md +19 -234
  26. package/agents-config/skills/apex/steps/step-08-run-tests.md +13 -300
  27. package/agents-config/skills/apex/steps/step-09-finish.md +36 -200
  28. package/agents-config/skills/apex/steps/step-10-verify.md +46 -264
  29. package/agents-config/skills/appstore-connect/agents/openai.yaml +7 -0
  30. package/agents-config/skills/appstore-connect/assets/codex-icon.svg +17 -0
  31. package/agents-config/skills/commit/agents/openai.yaml +10 -0
  32. package/agents-config/skills/commit/assets/codex-icon.svg +17 -0
  33. package/agents-config/skills/create-pr/agents/openai.yaml +10 -0
  34. package/agents-config/skills/create-pr/assets/codex-icon.svg +17 -0
  35. package/agents-config/skills/environments-manager/SKILL.md +1 -1
  36. package/agents-config/skills/environments-manager/agents/openai.yaml +7 -0
  37. package/agents-config/skills/environments-manager/assets/codex-icon.svg +16 -0
  38. package/agents-config/skills/environments-manager/examples/scripts/claude-worktree-remove.sh +19 -3
  39. package/agents-config/skills/environments-manager/examples/scripts/worktree-up.sh +1 -1
  40. package/agents-config/skills/environments-manager/references/claude.md +1 -1
  41. package/agents-config/skills/fix-pr-comments/agents/openai.yaml +10 -0
  42. package/agents-config/skills/fix-pr-comments/assets/codex-icon.svg +17 -0
  43. package/agents-config/skills/grill-me/SKILL.md +25 -4
  44. package/agents-config/skills/grill-me/agents/openai.yaml +8 -0
  45. package/agents-config/skills/grill-me/assets/codex-icon.svg +16 -0
  46. package/agents-config/skills/hooks-manager/SKILL.md +19 -9
  47. package/agents-config/skills/hooks-manager/assets/codex-icon.svg +15 -4
  48. package/agents-config/skills/hooks-manager/references/claude-code.md +32 -0
  49. package/agents-config/skills/hooks-manager/references/codex.md +23 -0
  50. package/agents-config/skills/hooks-manager/references/cursor.md +18 -0
  51. package/agents-config/skills/hooks-manager/references/hook-types.md +5 -3
  52. package/agents-config/skills/hooks-manager/references/input-output-schemas.md +2 -2
  53. package/agents-config/skills/hooks-manager/references/research-sources.md +25 -0
  54. package/agents-config/skills/hooks-manager/references/router.md +32 -0
  55. package/agents-config/skills/hooks-manager/references/troubleshooting.md +3 -3
  56. package/agents-config/skills/merge/agents/openai.yaml +10 -0
  57. package/agents-config/skills/merge/assets/codex-icon.svg +17 -0
  58. package/agents-config/skills/oneshot/SKILL.md +4 -0
  59. package/agents-config/skills/oneshot/agents/openai.yaml +10 -0
  60. package/agents-config/skills/oneshot/assets/codex-icon.svg +18 -0
  61. package/agents-config/skills/rules-manager/agents/openai.yaml +7 -0
  62. package/agents-config/skills/rules-manager/assets/codex-icon.svg +23 -0
  63. package/agents-config/skills/skill-manager/SKILL.md +45 -3
  64. package/agents-config/skills/skill-manager/agents/openai.yaml +7 -0
  65. package/agents-config/skills/skill-manager/assets/codex-icon.svg +23 -0
  66. package/agents-config/skills/skill-manager/references/skill-writing-glossary.md +201 -0
  67. package/agents-config/skills/skill-manager/scripts/setup-codex-icons.ts +143 -0
  68. package/agents-config/skills/ultrathink/agents/openai.yaml +10 -0
  69. package/agents-config/skills/ultrathink/assets/codex-icon.svg +20 -0
  70. package/agents-config/skills/use-artifacts/SKILL.md +102 -51
  71. package/agents-config/skills/use-artifacts/assets/local-runtime.js +299 -0
  72. package/agents-config/skills/use-artifacts/scripts/create_artifact.py +1 -1
  73. package/agents-config/skills/use-delegate/SKILL.md +4 -0
  74. package/agents-config/skills/use-delegate/agents/openai.yaml +10 -0
  75. package/agents-config/skills/use-delegate/assets/codex-icon.svg +20 -0
  76. package/agents-config/skills/use-goal/SKILL.md +70 -9
  77. package/agents-config/skills/use-goal/agents/openai.yaml +1 -1
  78. package/agents-config/skills/use-goal/assets/codex-icon.svg +17 -3
  79. package/agents-config/skills/use-goal/references/claude-code-goal.md +54 -6
  80. package/agents-config/skills/use-goal/references/codex-goal.md +59 -4
  81. package/agents-config/skills/use-goal/references/verification-harnesses.md +104 -3
  82. package/dist/cli.js +365 -363
  83. package/package.json +1 -1
  84. package/agents-config/skills/apex/templates/00-context.md +0 -55
  85. package/agents-config/skills/apex/templates/01-analyze.md +0 -10
  86. package/agents-config/skills/apex/templates/02-plan.md +0 -10
  87. package/agents-config/skills/apex/templates/03-execute.md +0 -10
  88. package/agents-config/skills/apex/templates/04-validate.md +0 -10
  89. package/agents-config/skills/apex/templates/05-examine.md +0 -10
  90. package/agents-config/skills/apex/templates/06-resolve.md +0 -10
  91. package/agents-config/skills/apex/templates/07-tests.md +0 -10
  92. package/agents-config/skills/apex/templates/08-run-tests.md +0 -10
  93. package/agents-config/skills/apex/templates/09-finish.md +0 -10
  94. package/agents-config/skills/apex/templates/10-verify.md +0 -9
  95. package/agents-config/skills/apex/templates/README.md +0 -195
  96. package/agents-config/skills/apex/templates/step-complete.md +0 -7
  97. package/agents-config/skills/prompt-creator/SKILL.md +0 -285
  98. package/agents-config/skills/prompt-creator/references/anthropic-best-practices.md +0 -126
  99. package/agents-config/skills/prompt-creator/references/anti-patterns.md +0 -57
  100. package/agents-config/skills/prompt-creator/references/clarity-principles.md +0 -54
  101. package/agents-config/skills/prompt-creator/references/context-management.md +0 -389
  102. package/agents-config/skills/prompt-creator/references/few-shot-patterns.md +0 -47
  103. package/agents-config/skills/prompt-creator/references/openai-best-practices.md +0 -50
  104. package/agents-config/skills/prompt-creator/references/prompt-templates.md +0 -110
  105. package/agents-config/skills/prompt-creator/references/reasoning-techniques.md +0 -52
  106. package/agents-config/skills/prompt-creator/references/system-prompt-patterns.md +0 -48
  107. package/agents-config/skills/prompt-creator/references/xml-structure.md +0 -36
@@ -1,154 +1,36 @@
1
1
  #!/bin/bash
2
- # APEX Template Setup Script
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 -e
4
+ set -euo pipefail
9
5
 
10
- # Arguments - first arg is now just the feature name (kebab-case)
11
- FEATURE_NAME="$1"
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
- # Validate required arguments
27
- if [[ -z "$FEATURE_NAME" ]]; then
28
- echo "Error: FEATURE_NAME is required"
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
- if [[ "$TASKS_MODE" == "true" ]]; then
143
- mkdir -p "${OUTPUT_DIR}/tasks"
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
- if [[ "$PR_MODE" == "true" ]]; then
147
- render_template "${TEMPLATE_DIR}/09-finish.md" "${OUTPUT_DIR}/09-finish.md"
148
- fi
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
- # Output the generated task_id for capture by caller
151
- echo "TASK_ID=${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 "✓ APEX templates initialized: ${OUTPUT_DIR}"
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
- # APEX Progress Update Script
3
- # Updates 00-context.md progress table
2
+ # Backward-compatible progress event writer for older APEX callers.
4
3
 
5
- set -e
4
+ set -euo pipefail
6
5
 
7
- # Arguments
8
- TASK_ID="$1"
9
- STEP_NUMBER="$2"
10
- STEP_NAME="$3"
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
- # Validate required arguments
14
- if [[ -z "$TASK_ID" ]] || [[ -z "$STEP_NUMBER" ]] || [[ -z "$STEP_NAME" ]] || [[ -z "$STATUS" ]]; then
15
- echo "Usage: $0 <task_id> <step_number> <step_name> <status>"
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
- CONTEXT_FILE="${PROJECT_ROOT}/.claude/output/apex/${TASK_ID}/00-context.md"
23
-
24
- # Validate context file exists
25
- if [[ ! -f "$CONTEXT_FILE" ]]; then
26
- echo "Error: Context file not found: $CONTEXT_FILE"
27
- exit 1
28
- fi
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}"