fix template, remove okr, use casan.*

This commit is contained in:
thanhnv
2026-07-18 16:45:31 +07:00
parent 0dfd1742d3
commit 13fae3e6c3
249 changed files with 4881 additions and 5702 deletions
@@ -11,6 +11,7 @@
"project_id": "AINative_OKR_CASAN4",
"domain": "SDD OKR",
"domain_root": "apps/okr/domain",
"manifest": "apps/okr/domain/project.manifest.json",
"context_roots": [
"apps/okr/domain",
"apps/okr/frontend",
@@ -41,6 +42,7 @@
"project_id": "CASAN_SERVICE_DESK",
"domain": "IT Service Desk",
"domain_root": "apps/service-desk/domain",
"manifest": "apps/service-desk/domain/project.manifest.json",
"context_roots": [
"apps/service-desk"
],
@@ -76,7 +76,12 @@ fi
# apps/okr/domain (compat symlinks bridge the pre-move .specify paths). A different
# app sets CASAN_DOMAIN_ROOT to its own apps/<project>/domain (Plan-06 reuse).
if [[ -z "${CASAN_DOMAIN_ROOT:-}" ]]; then
if [[ -d "$CASAN_APP_ROOT/apps/okr/domain" ]]; then
if [[ -n "${CASAN_PROJECT_MANIFEST:-}${CASAN_PROJECT_ID:-}" ]] && command -v python3 >/dev/null 2>&1; then
_casan_manifest_args=(domain-root --root "$CASAN_APP_ROOT")
[[ -n "${CASAN_PROJECT_MANIFEST:-}" ]] && _casan_manifest_args+=(--manifest "$CASAN_PROJECT_MANIFEST")
[[ -n "${CASAN_PROJECT_ID:-}" ]] && _casan_manifest_args+=(--project "$CASAN_PROJECT_ID")
CASAN_DOMAIN_ROOT="$(python3 "$CASAN_HARNESS_ROOT/scripts/bash/project_manifest.py" "${_casan_manifest_args[@]}")" || return 2
elif [[ -d "$CASAN_APP_ROOT/apps/okr/domain" ]]; then
CASAN_DOMAIN_ROOT="$CASAN_APP_ROOT/apps/okr/domain"
else
# Fallback for the pre-split monolithic layout: domain data co-located in .specify.
@@ -6,7 +6,9 @@ set -uo pipefail
# first because it rewrites `.specify/logs`.
#
# Env:
# CASAN_CI_RUN_FRONTEND=0|1 default 1
# CASAN_CI_RUN_PROJECT=0|1 default 1 (manifest build + test commands)
# CASAN_CI_RUN_BACKEND=0|1 default 0 (legacy workspace override)
# CASAN_CI_RUN_FRONTEND=0|1 default 0 (legacy workspace override)
# CASAN_CI_RUN_CONTROL_PANEL=0|1 default 1
# CASAN_CI_RUN_INFRA_LAB=0|1 default 0 (Docker Compose lab is optional in CI)
# CASAN_CI_STEP_TIMEOUT_SEC default 600
@@ -78,6 +80,7 @@ run "adversarial-harness" bash "$TESTS/adversarial-harness-tests.sh"
run "phase1-track-a" bash "$TESTS/phase1-track-a-tests.sh"
run "phase2-track-c" bash "$TESTS/phase2-track-c-tests.sh"
run "phase2-sourcegen" bash "$TESTS/phase2-sourcegen-tests.sh"
run "phase-project-shell" bash "$TESTS/phase-project-shell-tests.sh"
run "phase3-evidence-pack" bash "$TESTS/phase3-evidence-pack-tests.sh"
run "phase3-model-router" bash "$TESTS/phase3-model-router-tests.sh"
run "phase-h5-approval" bash "$TESTS/phase-h5-approval-tests.sh"
@@ -172,7 +175,15 @@ run "phase-chat-tenant" bash "$TESTS/phase-chat-tenant-tests.sh"
run "test-integrity" python3 "$SCRIPT_DIR/test-integrity.py" verify
run "bundle-integrity" python3 "$SCRIPT_DIR/bundle-integrity.py" verify
if [[ "${CASAN_CI_RUN_BACKEND:-1}" == "1" ]]; then
if [[ "${CASAN_CI_RUN_PROJECT:-1}" == "1" ]]; then
run "project-manifest" python3 "$SCRIPT_DIR/project_manifest.py" validate --root "$ROOT"
run "project-build" python3 "$SCRIPT_DIR/project_manifest.py" run --root "$ROOT" --kind build
run "project-test" python3 "$SCRIPT_DIR/project_manifest.py" run --root "$ROOT" --kind test
else
skip "project-build/test (CASAN_CI_RUN_PROJECT=0)"
fi
if [[ "${CASAN_CI_RUN_BACKEND:-0}" == "1" ]]; then
if command -v npm >/dev/null 2>&1; then
run "backend-tests" npm test -w backend
else
@@ -182,7 +193,7 @@ else
skip "backend-tests (CASAN_CI_RUN_BACKEND=0)"
fi
if [[ "${CASAN_CI_RUN_FRONTEND:-1}" == "1" ]]; then
if [[ "${CASAN_CI_RUN_FRONTEND:-0}" == "1" ]]; then
if command -v npm >/dev/null 2>&1; then
run "frontend-vitest" npm test -w frontend
else
@@ -21,6 +21,8 @@ import os
import subprocess
import sys
from project_manifest import load as load_project_manifest
# Plan-01: this script lives at <harness>/scripts/bash/; the harness root is two levels up.
_HARNESS = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", ".."))
@@ -51,6 +53,17 @@ def status_rc(env_key):
def main():
root, run_id, pack_dir = sys.argv[1:4]
try:
project = load_project_manifest(root)
domain_root = os.path.join(root, project["domain_root"])
requirements_path = os.path.join(root, project["requirements"])
feature_id = project["feature"]["id"]
except (OSError, ValueError):
domain_root = os.environ.get("CASAN_DOMAIN_ROOT", os.path.join(root, "apps/okr/domain"))
requirements_path = os.path.join(domain_root, "input", "requirement.md")
if not os.path.isfile(requirements_path):
requirements_path = os.path.join(domain_root, "input", "okr-requirement.md")
feature_id = "casan-demo"
os.makedirs(pack_dir, exist_ok=True)
logs = os.path.join(root, ".specify", "logs")
@@ -59,7 +72,7 @@ def main():
# H1 context
reports["h1-context-report.json"] = {
"harness": "H1-context", "run_id": run_id,
"context_yaml": os.path.exists(os.path.join(root, "docs/output/output_logs/casan-demo/pipeline-context.yaml")),
"context_yaml": os.path.exists(os.path.join(root, "docs/output/output_logs", feature_id, "pipeline-context.yaml")),
"note": "path/artifact validation performed by context-validate.sh at run time",
}
@@ -88,9 +101,9 @@ def main():
sys.executable,
traceability_script,
"--requirements",
os.path.join(root, "apps/okr/domain/input/okr-requirement.md"),
requirements_path,
"--map",
os.path.join(root, "apps/okr/domain/traceability-map.json"),
os.path.join(domain_root, "traceability-map.json"),
"--out",
traceability_out,
"--gate",
@@ -148,7 +161,7 @@ def main():
fp = json.load(open(fp_json, encoding="utf-8"))
except ValueError:
fp = None
vectors_path = os.path.join(root, "apps/okr/domain/corpus/redteam-vectors.jsonl")
vectors_path = os.path.join(domain_root, "corpus", "redteam-vectors.jsonl")
vectors = read_jsonl(vectors_path)
reports["redteam-result.json"] = {
"run_id": run_id, "vectors_defined": len(vectors),
@@ -1,11 +1,14 @@
#!/usr/bin/env python3
"""Apply an approved Goal patch with bounded paths and rollback-on-failure."""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import subprocess
import tempfile
import importlib.util
from datetime import datetime, timezone
@@ -20,6 +23,12 @@ def root() -> str:
ROOT = root()
INBOX = os.path.join(ROOT, "packages", "casan-harness", "scripts", "bash", "approval-inbox.py")
_MANIFEST_SPEC = importlib.util.spec_from_file_location(
"casan_project_manifest",
os.path.join(os.path.dirname(__file__), "project_manifest.py"),
)
PROJECT_MANIFEST = importlib.util.module_from_spec(_MANIFEST_SPEC)
_MANIFEST_SPEC.loader.exec_module(PROJECT_MANIFEST)
def now() -> str:
@@ -62,15 +71,33 @@ def verify_approval(job: dict) -> dict:
return proposal
def verification_commands(files: list[str]) -> list[list[str]]:
commands = [["git", "diff", "--check", "--", *files]]
if any(path.startswith("apps/okr/frontend/") for path in files):
commands.append(["npm", "run", "build", "-w", "@ainative-okr/frontend"])
commands.append(["npm", "test", "-w", "@ainative-okr/frontend"])
if any(path.startswith("apps/okr/backend/") for path in files):
commands.append(["npm", "run", "build", "-w", "@ainative-okr/backend"])
commands.append(["npm", "test", "-w", "@ainative-okr/backend"])
return commands
def _manifest_for_files(files: list[str]) -> dict:
selected = os.environ.get("CASAN_PROJECT_MANIFEST")
project = os.environ.get("CASAN_PROJECT_ID")
if selected or project:
return PROJECT_MANIFEST.load(ROOT, selected, project)
registry = load(os.path.join(ROOT, "packages", "casan-harness", "level5", "project-registry.json"))
candidates = []
for entry in registry.get("projects", []):
manifest_path = entry.get("manifest")
if not manifest_path or entry.get("status") != "active":
continue
try:
manifest = PROJECT_MANIFEST.load(ROOT, manifest_path)
except (OSError, ValueError):
continue
if any(any(path.startswith(root.rstrip("/") + "/") for root in manifest["source_roots"]) for path in files):
candidates.append(manifest)
if len(candidates) != 1:
reason = "none" if not candidates else "multiple"
raise RuntimeError(f"GOAL_APPLY_PROJECT_MANIFEST_{reason.upper()}")
return candidates[0]
def verification_commands(files: list[str], manifest: dict | None = None) -> list[list[str]]:
project = manifest or _manifest_for_files(files)
return [["git", "diff", "--check", "--", *files], *PROJECT_MANIFEST.verification_commands(project, files)]
def execute(job_path: str, actor: str) -> dict:
@@ -0,0 +1,69 @@
#!/usr/bin/env bash
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/casan-paths.sh"
ROOT="$CASAN_APP_ROOT"
MANIFEST_TOOL="$SCRIPT_DIR/project_manifest.py"
PASS=0 FAIL=0 SKIP=0
manifest_args=(--root "$ROOT")
[[ -n "${CASAN_PROJECT_MANIFEST:-}" ]] && manifest_args+=(--manifest "$CASAN_PROJECT_MANIFEST")
[[ -n "${CASAN_PROJECT_ID:-}" ]] && manifest_args+=(--project "$CASAN_PROJECT_ID")
run() {
local name="$1"; shift
echo "==> $name"
if "$@"; then
echo "PROJECT_GATE_PASS $name"; PASS=$((PASS + 1))
else
echo "PROJECT_GATE_FAIL $name" >&2; FAIL=$((FAIL + 1))
fi
}
run "manifest" python3 "$MANIFEST_TOOL" validate "${manifest_args[@]}"
if [[ "$FAIL" -ne 0 ]]; then
echo "PROJECT_GATE_SUMMARY PASS=$PASS FAIL=$FAIL SKIP=$SKIP"
exit 1
fi
project_id="$(python3 "$MANIFEST_TOOL" get "${manifest_args[@]}" --field project_id)"
requirements="$(python3 "$MANIFEST_TOOL" get "${manifest_args[@]}" --field requirements)"
architecture="$(python3 "$MANIFEST_TOOL" get "${manifest_args[@]}" --field architecture)"
domain_root="$(python3 "$MANIFEST_TOOL" domain-root "${manifest_args[@]}")"
evidence_dir="$ROOT/docs/output/casan/$project_id/project-gate"
mkdir -p "$evidence_dir"
run "requirement-security" bash "$SCRIPT_DIR/artifact-scan.sh" "$ROOT/$requirements" "$project_id-requirements"
run "architecture-security" bash "$SCRIPT_DIR/artifact-scan.sh" "$ROOT/$architecture" "$project_id-architecture"
if [[ "${CASAN_PROJECT_GATE_RUN_CORPUS:-1}" == "1" ]]; then
run "corpus-quality" env CASAN_DOMAIN_ROOT="$domain_root" bash "$SCRIPT_DIR/benign-fp-report.sh" "$evidence_dir/benign-fp-report.json"
else
echo "PROJECT_GATE_SKIP corpus-quality"; SKIP=$((SKIP + 1))
fi
run "traceability" python3 "$SCRIPT_DIR/traceability-matrix.py" \
--requirements "$ROOT/$requirements" \
--map "$domain_root/traceability-map.json" \
--out "$evidence_dir/traceability-matrix.json" --gate
if [[ "${CASAN_PROJECT_GATE_RUN_BUILD:-1}" == "1" ]]; then
run "build" python3 "$MANIFEST_TOOL" run "${manifest_args[@]}" --kind build
else
echo "PROJECT_GATE_SKIP build"; SKIP=$((SKIP + 1))
fi
if [[ "${CASAN_PROJECT_GATE_RUN_TEST:-1}" == "1" ]]; then
run "test" python3 "$MANIFEST_TOOL" run "${manifest_args[@]}" --kind test
else
echo "PROJECT_GATE_SKIP test"; SKIP=$((SKIP + 1))
fi
python3 - "$evidence_dir/summary.json" "$project_id" "$PASS" "$FAIL" "$SKIP" <<'PY'
import json, sys
path, project, passed, failed, skipped = sys.argv[1:]
with open(path, "w", encoding="utf-8") as handle:
json.dump({"project_id": project, "pass": int(passed), "fail": int(failed), "skip": int(skipped), "accepted": int(failed) == 0}, handle, indent=2)
handle.write("\n")
PY
echo "PROJECT_GATE_SUMMARY project=$project_id PASS=$PASS FAIL=$FAIL SKIP=$SKIP evidence=$evidence_dir"
[[ "$FAIL" -eq 0 ]]
@@ -0,0 +1,213 @@
#!/usr/bin/env python3
"""Strict, dependency-free reader for the CASAN project manifest contract."""
from __future__ import annotations
import json
import os
import re
import subprocess
import sys
import argparse
from pathlib import Path
class ManifestError(ValueError):
pass
def _fail(message: str) -> None:
raise ManifestError(f"CASAN_PROJECT_MANIFEST_INVALID: {message}")
def safe_relative(value: object, label: str) -> str:
if not isinstance(value, str) or not value or "\x00" in value or "\\" in value or os.path.isabs(value):
_fail(f"{label} must be a non-empty repository-relative POSIX path")
normalized = os.path.normpath(value).replace(os.sep, "/")
if normalized == ".." or normalized.startswith("../") or "/../" in normalized:
_fail(f"{label} escapes the repository root")
return normalized.removeprefix("./")
def within(root: str, value: object, label: str, must_exist: bool = True) -> str:
relative = safe_relative(value, label)
root_real = os.path.realpath(root)
candidate = os.path.realpath(os.path.join(root_real, relative)) if must_exist else os.path.abspath(os.path.join(root_real, relative))
if os.path.commonpath([root_real, candidate]) != root_real:
_fail(f"{label} resolves outside the repository root")
if must_exist and not os.path.exists(candidate):
_fail(f"{label} does not exist: {relative}")
return candidate
def _json(path: str, label: str) -> dict:
try:
with open(path, encoding="utf-8") as handle:
value = json.load(handle)
except (OSError, json.JSONDecodeError) as error:
_fail(f"{label} is not valid JSON: {error}")
if not isinstance(value, dict):
_fail(f"{label} must be an object")
return value
def _commands(value: object, label: str, allowed: set[str]) -> list[list[str]]:
if not isinstance(value, list):
_fail(f"{label} must be an array")
result = []
for index, command in enumerate(value):
if not isinstance(command, list) or not command or any(not isinstance(part, str) or not part for part in command):
_fail(f"{label}[{index}] must be a non-empty argv array")
if command[0] not in allowed:
_fail(f"{label}[{index}] executable is not allowed: {command[0]}")
result.append(command.copy())
return result
def load(root: str, manifest_path: str | None = None, project_id: str | None = None) -> dict:
root = os.path.realpath(root)
selected = manifest_path or os.environ.get("CASAN_PROJECT_MANIFEST")
requested = project_id or os.environ.get("CASAN_PROJECT_ID")
if selected:
path = within(root, selected, "manifest")
elif requested:
registry = _json(os.path.join(root, "packages/casan-harness/level5/project-registry.json"), "project registry")
entry = next((item for item in registry.get("projects", []) if item.get("project_id") == requested), None)
if not entry:
_fail(f"project is not registered: {requested}")
selected = entry.get("manifest") or f"{safe_relative(entry.get('domain_root'), 'registry domain_root')}/project.manifest.json"
path = within(root, selected, "registered manifest")
else:
path = within(root, "apps/okr/domain/project.manifest.json", "default manifest")
raw = _json(path, "project manifest")
if raw.get("schema_version") != 1:
_fail("schema_version must be 1")
if not re.fullmatch(r"[a-z][a-z0-9-]{1,62}", str(raw.get("project_id", ""))):
_fail("project_id must be a lowercase slug")
feature = raw.get("feature")
if not isinstance(feature, dict) or not re.fullmatch(r"[0-9]{3}-[a-z0-9-]+", str(feature.get("id", ""))):
_fail("feature.id must match NNN-slug")
if not re.fullmatch(r"MOD-[0-9]{2,}", str(feature.get("module_id", ""))):
_fail("feature.module_id must match MOD-NN")
for key in ("domain_root", "requirements", "architecture", "quality_profile"):
raw[key] = safe_relative(raw.get(key), key)
within(root, raw[key], key)
raw["artifacts_root"] = safe_relative(raw.get("artifacts_root", "docs/output"), "artifacts_root")
source_roots = raw.get("source_roots")
if not isinstance(source_roots, list) or not source_roots:
_fail("source_roots must contain at least one path")
raw["source_roots"] = list(dict.fromkeys(safe_relative(item, "source_roots") for item in source_roots))
for item in raw["source_roots"]:
within(root, item, "source_root")
profile = _json(within(root, raw["quality_profile"], "quality_profile"), "quality profile")
allowed = set(profile.get("allowed_command_executables", []))
if profile.get("schema_version") != 1 or not allowed:
_fail("quality profile version or command allowlist is invalid")
raw["quality"] = profile
raw["commands"] = {
"build": _commands(raw.get("commands", {}).get("build"), "commands.build", allowed),
"test": _commands(raw.get("commands", {}).get("test"), "commands.test", allowed),
}
if profile.get("require_build_commands") and not raw["commands"]["build"]:
_fail("build commands are required by the quality profile")
if profile.get("require_test_commands") and not raw["commands"]["test"]:
_fail("test commands are required by the quality profile")
verification = raw.get("verification")
if not isinstance(verification, list) or (profile.get("require_verification_mapping") and not verification):
_fail("verification mapping is required")
normalized_rules = []
for index, rule in enumerate(verification):
if not isinstance(rule, dict):
_fail(f"verification[{index}] must be an object")
prefix = safe_relative(rule.get("path_prefix"), f"verification[{index}].path_prefix").rstrip("/") + "/"
normalized_rules.append({"path_prefix": prefix, "commands": _commands(rule.get("commands"), f"verification[{index}].commands", allowed)})
raw["verification"] = normalized_rules
if profile.get("fail_on_unmapped_source_root"):
for source in raw["source_roots"]:
prefix = source.rstrip("/") + "/"
if not any(prefix.startswith(rule["path_prefix"]) or rule["path_prefix"].startswith(prefix) for rule in normalized_rules):
_fail(f"source root has no verification rule: {source}")
raw["manifest_path"] = os.path.relpath(path, root).replace(os.sep, "/")
raw["root"] = root
return raw
def verification_commands(manifest: dict, files: list[str]) -> list[list[str]]:
commands: list[list[str]] = []
seen: set[tuple[str, ...]] = set()
for rule in manifest["verification"]:
if any(safe_relative(path, "changed file").startswith(rule["path_prefix"]) for path in files):
for command in rule["commands"]:
key = tuple(command)
if key not in seen:
commands.append(command.copy())
seen.add(key)
return commands
def run_commands(manifest: dict, kind: str) -> int:
if kind not in {"build", "test"}:
_fail(f"unsupported command kind: {kind}")
for command in manifest["commands"][kind]:
print(f"CASAN_PROJECT_COMMAND kind={kind} argv={json.dumps(command, ensure_ascii=False)}", flush=True)
timeout = int(os.environ.get("CASAN_PROJECT_COMMAND_TIMEOUT_SEC", "600"))
try:
result = subprocess.run(command, cwd=manifest["root"], check=False, timeout=timeout)
except subprocess.TimeoutExpired:
print(f"CASAN_PROJECT_COMMAND_TIMEOUT kind={kind} seconds={timeout}", file=sys.stderr)
return 124
if result.returncode != 0:
print(f"CASAN_PROJECT_COMMAND_FAILED kind={kind} rc={result.returncode}", file=sys.stderr)
return result.returncode
return 0
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("action", choices=["validate", "get", "domain-root", "run", "verification"])
parser.add_argument("--root", default=os.getcwd())
parser.add_argument("--manifest")
parser.add_argument("--project")
parser.add_argument("--kind", choices=["build", "test"])
parser.add_argument("--file", action="append", default=[])
parser.add_argument("--field", choices=["project_id", "requirements", "architecture", "domain_root", "quality_profile"])
args = parser.parse_args()
try:
manifest = load(args.root, args.manifest, args.project)
if args.action == "domain-root":
print(os.path.join(manifest["root"], manifest["domain_root"]))
return 0
if args.action == "get":
if not args.field:
_fail("--field is required for get")
print(manifest[args.field])
return 0
if args.action == "validate":
print(json.dumps({
"status": "valid",
"project_id": manifest["project_id"],
"manifest": manifest["manifest_path"],
"quality_profile": manifest["quality"]["profile_id"],
"build_commands": len(manifest["commands"]["build"]),
"test_commands": len(manifest["commands"]["test"]),
"verification_rules": len(manifest["verification"]),
}, ensure_ascii=False))
return 0
if args.action == "run":
if not args.kind:
_fail("--kind is required for run")
return run_commands(manifest, args.kind)
print(json.dumps(verification_commands(manifest, args.file), ensure_ascii=False))
return 0
except (ManifestError, OSError, ValueError) as error:
print(str(error), file=sys.stderr)
return 2
if __name__ == "__main__":
raise SystemExit(main())
@@ -110,9 +110,14 @@ def resolve_files(root: str, values):
def main() -> int:
root = project_root()
domain = os.environ.get("CASAN_DOMAIN_ROOT", os.path.join(root, "apps/okr/domain"))
if not os.path.isabs(domain):
domain = os.path.join(root, domain)
generic_requirement = os.path.join(domain, "input", "requirement.md")
legacy_requirement = os.path.join(domain, "input", "okr-requirement.md")
ap = argparse.ArgumentParser()
ap.add_argument("--requirements", default=os.path.join(root, "apps/okr/domain/input/okr-requirement.md"))
ap.add_argument("--map", default=os.path.join(root, "apps/okr/domain/traceability-map.json"))
ap.add_argument("--requirements", default=generic_requirement if os.path.isfile(generic_requirement) else legacy_requirement)
ap.add_argument("--map", default=os.path.join(domain, "traceability-map.json"))
ap.add_argument("--out", default=os.path.join(root, "docs/output/casan/traceability-matrix.json"))
ap.add_argument("--gate", action="store_true")
args = ap.parse_args()
@@ -24,19 +24,19 @@ OUT = ROOT / "docs" / "output" / "output_logs" / "casan-demo" / "pipeline-contex
steps = [
("step-0", "detect-existing-spec", "agent_step"),
("step-1-srs", "okr.srs", "agent_step"),
("step-2-bd", "okr.bd", "agent_step"),
("step-1-srs", "casan.srs", "agent_step"),
("step-2-bd", "casan.bd", "agent_step"),
("step-3-spec", "speckit.specify", "agent_step"),
("step-4-clarify", "speckit.clarify", "agent_step"),
("step-5-review-spec", "okr.reviewspec", "agent_step"),
("step-5-review-spec", "casan.reviewspec", "agent_step"),
("step-6-plan", "speckit.plan", "agent_step"),
("step-7-review-plan", "okr.reviewplan", "agent_step"),
("step-8-dd", "okr.dd", "agent_step"),
("step-8b-testcases", "okr.testkit.gen-testcases", "agent_step"),
("step-7-review-plan", "casan.reviewplan", "agent_step"),
("step-8-dd", "casan.dd", "agent_step"),
("step-8b-testcases", "casan.testkit.gen-testcases", "agent_step"),
("step-9-tasks", "speckit.tasks", "agent_step"),
("step-10-implement", "speckit.implement", "write_code"),
("step-11-review-code", "okr.reviewcode", "agent_step"),
("step-12-testkit", "okr.testkit.run-tests", "agent_step"),
("step-11-review-code", "casan.reviewcode", "agent_step"),
("step-12-testkit", "casan.testkit.run-tests", "agent_step"),
("step-13-launch", "boss.launch", "deploy"),
]
@@ -202,5 +202,11 @@ class GoalPatchWorkflowTests(unittest.TestCase):
self.assertIn(["npm", "run", "build", "-w", "@ainative-okr/frontend"], commands)
self.assertIn(["npm", "test", "-w", "@ainative-okr/frontend"], commands)
def test_service_desk_patch_uses_service_desk_manifest_commands(self):
commands = EXECUTOR.verification_commands(["apps/service-desk/src/ticket.js"])
self.assertIn(["node", "--check", "apps/service-desk/src/ticket.js"], commands)
self.assertIn(["node", "--test", "apps/service-desk/test/ticket.test.mjs"], commands)
self.assertNotIn(["npm", "test", "-w", "@ainative-okr/backend"], commands)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../scripts/bash/casan-paths.sh"
python3 "$CASAN_APP_ROOT/packages/casan-devkit/tests/project-scaffold-tests.py"
python3 "$CASAN_HARNESS_ROOT/scripts/bash/project_manifest.py" validate \
--root "$CASAN_APP_ROOT" --manifest apps/okr/domain/project.manifest.json >/dev/null
python3 "$CASAN_HARNESS_ROOT/scripts/bash/project_manifest.py" validate \
--root "$CASAN_APP_ROOT" --manifest apps/service-desk/domain/project.manifest.json >/dev/null
node --check "$CASAN_APP_ROOT/scripts/casan-project.mjs"
node --check "$CASAN_APP_ROOT/scripts/casan-step.mjs"
node --check "$CASAN_APP_ROOT/scripts/run-casan-pipeline.mjs"
echo "PROJECT_SHELL_TESTS_PASS"