Skip to content
4 changes: 2 additions & 2 deletions references/assets/orchestration/contract/plan-v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,5 +157,5 @@ If any generated artifact drifts from the source specification, omits required s

## 10. Related Specifications / Further Reading

- [Related specification](.work-bundle/orchestration/spec/active/...)
- [Carried durable-knowledge context, if any, from the source specification](.work-bundle/orchestration/spec/active/...)
- Related specification: `.work-bundle/orchestration/spec/active/...`
- Carried durable-knowledge context, if any: source specification front matter
7 changes: 7 additions & 0 deletions rules/orchestration/orch-review-completion.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ Keep final review focused on whether the WorkBundle workflow completed correctly
- Require the execution-evidence-driven final Knowledge Base Update disposition to be `completed` or `not-needed` before archive; archive remains blocked while promoted closure lacks validated keep-summarizing return evidence.
- Create or require plan repair only for a decomposition defect, and specification repair only for a requirement, design, or authority defect.
- Complete allowed commit, applicable CodeGraph sync, metadata update, archive, and index refresh only after all gates allow finalization.
- When a post-execution runtime or UI defect is classified, or the accepted specification or plan explicitly claims runtime acceptance of a user-visible invariant, require a `RuntimeVerificationClassificationV1` before archive. Evaluate the original user request and accepted specification before the plan, task acceptance criteria, executor handoffs, produced commits, and execution-introduced behavior.
- Require `RuntimeVerificationClassificationV1` to carry `classification`, `invariant_trace`, `negative_evidence`, and `owning_repair`. Accepted classes are `execution_introduced_bug`, `implementation_gap`, `new_feature`, and `uncovered_fixture`.
- For an accepted-invariant `execution_introduced_bug` or `implementation_gap`, require `invariant_trace` to connect original requirement, specification invariant, owning plan task or acceptance criterion, changed commit, materialization, presentation, and runtime or UI proof. Passing component or unit tests alone is insufficient for this triggered runtime claim; do not impose a universal browser or UI gate when neither trigger applies.
- Permit `new_feature` or `uncovered_fixture` with an empty `invariant_trace` only when `negative_evidence` proves no matching original user request or accepted specification invariant and no plan, handoff, or produced-commit contradiction.
- Route `owning_repair` to the first broken artifact: task or acceptance criterion present plus implementation miss means task repair and re-review; accepted specification present plus plan omission means plan repair and resume from the owning step; original-request invariant omitted or contradicted by the accepted specification means specification repair. Only after those cases are excluded may a residual class stand.
- Keep classification agent-owned and evidence-linked. A helper may require and structurally validate the record but must not decide the semantic class.
- Keep same-scope specification-owned handling authoritative for a first-observed classification defect. Persist separate WorkBundle violation evidence only after `wb-violation-evaluation` classifies the finding as work-bundle-scoped or mixed and same-scope specification-owned handling no longer applies.

## Must Not

Expand Down
95 changes: 95 additions & 0 deletions scripts/ks.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,100 @@
from __future__ import annotations

import importlib.util
import os
import shutil
import sys
import tomllib
from collections.abc import Mapping, Sequence
from importlib import metadata as importlib_metadata
from pathlib import Path

RUNTIME_DEPENDENCIES = (
("yaml", "pyyaml"),
("sqlite_vec", "sqlite-vec"),
("fastembed", "fastembed"),
)
UV_REEXEC_ENV = "WORK_BUNDLE_KS_UV_REEXEC"


def _declared_runtime_versions() -> dict[str, str]:
metadata_lines: list[str] = []
inside_script_metadata = False
for line in Path(__file__).read_text(encoding="utf-8").splitlines():
if line == "# /// script":
inside_script_metadata = True
continue
if inside_script_metadata and line == "# ///":
break
if inside_script_metadata:
metadata_lines.append(line.removeprefix("#").lstrip())

metadata = tomllib.loads("\n".join(metadata_lines))
versions: dict[str, str] = {}
for dependency in metadata.get("dependencies", []):
distribution, separator, version = dependency.partition("==")
if separator:
versions[distribution.lower().replace("_", "-")] = version
return versions


def _missing_runtime_dependencies() -> list[str]:
declared_versions = _declared_runtime_versions()
invalid: list[str] = []
for module_name, distribution_name in RUNTIME_DEPENDENCIES:
if importlib.util.find_spec(module_name) is None:
invalid.append(module_name)
continue
expected_version = declared_versions.get(distribution_name)
try:
actual_version = importlib_metadata.version(distribution_name)
except importlib_metadata.PackageNotFoundError:
invalid.append(f"{module_name} ({distribution_name} distribution missing)")
continue
if expected_version is None:
invalid.append(f"{module_name} ({distribution_name} is not pinned)")
elif actual_version != expected_version:
invalid.append(
f"{module_name} ({distribution_name} {actual_version} != {expected_version})"
)
return invalid


def _ensure_managed_runtime(
*,
argv: Sequence[str] | None = None,
environ: Mapping[str, str] | None = None,
) -> tuple[bool, str | None]:
missing = _missing_runtime_dependencies()
if not missing:
return True, None

current_environment = dict(os.environ if environ is None else environ)
missing_list = ", ".join(missing)
if current_environment.get(UV_REEXEC_ENV) == "1":
return (
False,
"KS_RUNTIME_DEPENDENCY_UNAVAILABLE: uv could not hydrate the declared "
f"runtime dependencies: {missing_list}",
)

uv_path = shutil.which("uv")
if uv_path is None:
return (
False,
"KS_RUNTIME_DEPENDENCY_UNAVAILABLE: missing runtime dependencies "
f"({missing_list}); install uv and retry this command",
)

current_argv = list(sys.argv if argv is None else argv)
current_environment[UV_REEXEC_ENV] = "1"
os.execve(
uv_path,
[uv_path, "run", str(Path(__file__).resolve()), *current_argv[1:]],
current_environment,
)
raise RuntimeError("uv runtime re-exec returned unexpectedly")


def _load_main():
module_path = Path(__file__).resolve().parent / "keep-summarizing" / "dispatcher.py"
Expand All @@ -28,6 +119,10 @@ def _load_main():


def main() -> int:
ready, error = _ensure_managed_runtime()
if not ready:
print(error, file=sys.stderr)
return 2
return int(_load_main()())


Expand Down
2 changes: 1 addition & 1 deletion scripts/orchestration/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from pathlib import Path


SPEC_STATUSES = {"draft", "active", "implemented", "reviewed", "superseded", "archived"}
SPEC_STATUSES = {"draft", "active", "verified", "implemented", "reviewed", "superseded", "archived"}
PLAN_STATUSES = {"Planned", "In progress", "Completed", "Deprecated", "On Hold"}
HANDOFF_STATUSES = {"active", "reviewed", "archived", "superseded"}
HANDOFF_TYPES = {"orchestration", "executor-result"}
Expand Down
6 changes: 5 additions & 1 deletion scripts/orchestration/dispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ def build_parser() -> argparse.ArgumentParser:
repository_preflight.add_argument("--task-file", action="append", default=[])
repository_preflight.add_argument("--reference", action="append", default=[])
repository_preflight.add_argument("--repository", action="append", default=[])
repository_preflight.add_argument("--accepted-baseline")
repository_preflight.add_argument(
"--accepted-baseline",
help="JSON file containing accepted repository baselines that reconcile observed branch or commit drift",
)
repository_preflight.set_defaults(func=cmd_repository_preflight)
task_brief = sub.add_parser("build-task-brief", parents=[parent])
task_brief.add_argument("--task", required=True)
Expand Down Expand Up @@ -91,6 +94,7 @@ def build_parser() -> argparse.ArgumentParser:
set_plan.add_argument("--id", required=True)
set_plan.add_argument("--status", required=True)
set_plan.add_argument("--kind", choices=["plan", "phase", "task"])
set_plan.add_argument("--plan-id")
set_plan.add_argument("--handoff")
set_plan.set_defaults(func=cmd_set_plan_status)
archive_plan = sub.add_parser("archive-plan", parents=[parent])
Expand Down
3 changes: 2 additions & 1 deletion scripts/orchestration/doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,8 @@ def cmd_doctor(args: argparse.Namespace) -> None:
[
"Disposable task briefs, review packages, and lightweight development plans",
"build-task-brief",
"independent dev-code-review",
"optional task review",
"acceptance_review.required: true",
"A task becomes `Completed` only when",
"Final workflow audit",
],
Expand Down
13 changes: 11 additions & 2 deletions scripts/orchestration/execution_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -1607,6 +1607,16 @@ def _task_context(args: argparse.Namespace) -> tuple[Path, Path, dict[str, Any],
return root, task_path, task_data, task_body, records, source_paths


def _contains_resolved_source_record(value: Any, record: str) -> bool:
if isinstance(value, str):
return record in value
if isinstance(value, dict):
return any(_contains_resolved_source_record(item, record) for item in value.values())
if isinstance(value, list):
return any(_contains_resolved_source_record(item, record) for item in value)
return False


def _compile_task_brief(args: argparse.Namespace) -> tuple[Path, dict[str, Any]]:
root, task_path, task, task_body, records, source_paths = _task_context(args)
task_id = _artifact_id(task, "id", task_path)
Expand Down Expand Up @@ -1700,9 +1710,8 @@ def _compile_task_brief(args: argparse.Namespace) -> tuple[Path, dict[str, Any]]
"review_required": review_required,
}
}
serialized_brief = json.dumps(brief, ensure_ascii=False)
for identifier in source_ids:
if records[identifier] not in serialized_brief:
if not _contains_resolved_source_record(brief, records[identifier]):
raise SystemExit(
f"Source ID {identifier} from {', '.join(path.as_posix() for path in source_paths)} "
"is not allocated to a resolved task-brief field"
Expand Down
28 changes: 26 additions & 2 deletions scripts/orchestration/plans.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
unique_explicit_handoff_plan_id,
validate_executor_result_for_task,
_compile_task_brief,
_parse_scalar,
)
from handoffs import _read_compact_yaml_metadata
from repository_preflight import capture_repository_evidence, task_caused_paths
from specs import load_index, replace_front_matter_value

Expand Down Expand Up @@ -59,6 +61,14 @@ def _plan_executor_handoffs(args: argparse.Namespace, plan_id: str) -> list[dict
for path in sorted(handoff_root.glob("*/*")):
if not path.is_file() or path.suffix not in {".yaml", ".yml"}:
continue
compact = _read_compact_yaml_metadata(path)
if isinstance(compact.get("related"), str):
related = _parse_scalar(str(compact["related"]))
if isinstance(related, dict):
compact["related"] = related
compact_plan_id = unique_explicit_handoff_plan_id(compact)
if compact_plan_id is not None and compact_plan_id != plan_id:
continue
handoff = read_structured_artifact(path)
if unique_explicit_handoff_plan_id(handoff) != plan_id:
continue
Expand Down Expand Up @@ -522,11 +532,25 @@ def cmd_set_plan_status(args: argparse.Namespace) -> None:
if args.status not in PLAN_STATUSES:
raise SystemExit(f"Invalid plan status: {args.status}")
rows = index_plans(args)
matches = [row for row in rows if row.get("id") == args.id and (not args.kind or row.get("type") == args.kind)]
kind = getattr(args, "kind", None)
plan_id = getattr(args, "plan_id", None)
matches = [
row
for row in rows
if row.get("id") == args.id
and (not kind or row.get("type") == kind)
and (not plan_id or row.get("plan_id") == plan_id)
]
if not matches:
raise SystemExit(f"Plan artifact not found: {args.id}")
if len(matches) > 1:
raise SystemExit(f"Multiple plan artifacts match {args.id}; pass --kind plan|phase|task")
selectors = []
if not kind:
selectors.append("--kind plan|phase|task")
if not plan_id:
selectors.append("--plan-id PLAN_ID")
guidance = f"; pass {' and '.join(selectors)}" if selectors else "; supplied selectors remain ambiguous"
raise SystemExit(f"Multiple plan artifacts match {args.id}{guidance}")
row = matches[0]
path = artifact_path_from_row(row, args)
if args.status == "Completed" and row.get("type") == "task":
Expand Down
9 changes: 8 additions & 1 deletion scripts/orchestration/repository_preflight.py
Original file line number Diff line number Diff line change
Expand Up @@ -636,7 +636,14 @@ def repository_preflight(
def _load_baselines(path: str | None) -> dict[str, list[str]]:
if not path:
return {}
value = json.loads(Path(path).read_text(encoding="utf-8"))
try:
raw = Path(path).read_text(encoding="utf-8")
except OSError as error:
raise SystemExit(f"Accepted baseline file could not be read: {path}: {error}") from error
try:
value = json.loads(raw)
except json.JSONDecodeError as error:
raise SystemExit(f"Accepted baseline must contain valid JSON: {path}: {error.msg}") from error
if not isinstance(value, dict) or not all(isinstance(item, list) for item in value.values()):
raise SystemExit("Accepted baseline must be a JSON object mapping repository paths to change lists.")
return {str(Path(key).resolve()): [str(change) for change in changes] for key, changes in value.items()}
Expand Down
54 changes: 50 additions & 4 deletions scripts/work-bundle/control_plane.py
Original file line number Diff line number Diff line change
Expand Up @@ -1083,6 +1083,26 @@ def _publish_evidence(control: Path, payload: dict[str, object]) -> Path:
return path


def _control_plane_gitlink_paths(control: Path) -> list[str]:
paths = {
str(marker.parent.relative_to(control)).replace("\\", "/")
for marker in control.rglob(".git")
if marker.parent != control and (marker.is_dir() or marker.is_file())
}
if (control / ".git").exists():
result = subprocess.run(
["git", "-C", str(control), "ls-files", "--stage", "-z"],
check=False,
capture_output=True,
)
if result.returncode == 0:
for entry in result.stdout.split(b"\0"):
if not entry.startswith(b"160000 ") or b"\t" not in entry:
continue
paths.add(entry.split(b"\t", 1)[1].decode("utf-8", errors="surrogateescape"))
return sorted(paths)


def cmd_publish_control_plane(args: list[str]) -> int:
parser = argparse.ArgumentParser(prog="wb.py publish-control-plane")
parser.add_argument("workspace_root")
Expand All @@ -1109,6 +1129,16 @@ def cmd_publish_control_plane(args: list[str]) -> int:
if not remote:
out({"command": "publish-control-plane", "status": "issues-found", "failure_code": "WB_CONTROL_PLANE_REMOTE_REQUIRED", "changed_files": []})
return 1
gitlink_paths = _control_plane_gitlink_paths(control)
if gitlink_paths:
out({
"command": "publish-control-plane",
"status": "issues-found",
"failure_code": "WB_CONTROL_PLANE_GITLINK_FORBIDDEN",
"gitlink_paths": gitlink_paths,
"changed_files": [],
})
return 1
if parsed.dry_run:
out({"command": "publish-control-plane", "status": "passed", "dry_run": True, "remote": remote, "changed_files": [], "git_actions": ["init", "configure-origin", "commit", "push"]})
return 0
Expand Down Expand Up @@ -1763,7 +1793,12 @@ def _materialize_workspace_root(remote: str, workspace_root: Path, default_branc


def _attach(
workspace_root: Path, materialize: str, repository_paths: dict[str, Path], apply: bool
workspace_root: Path,
materialize: str,
repository_paths: dict[str, Path],
apply: bool,
*,
create_script_index: bool = True,
) -> tuple[dict[str, object], int]:
metadata_path = workspace_root / ".work-bundle/project.yaml"
text = read(metadata_path)
Expand Down Expand Up @@ -1943,7 +1978,10 @@ def rollback_attach() -> None:
raise ControlPlaneError("WB_CONTROL_PLANE_TRANSACTION_FAILED") from exc
if apply:
try:
changed = ensure_workspace_resources(workspace_root)
changed = ensure_workspace_resources(
workspace_root,
create_script_index=create_script_index,
)
for relative in (".work-bundle/git", ".work-bundle/runtime", ".work-bundle/orchestration/execution-state"):
path = workspace_root / relative
if not path.exists():
Expand Down Expand Up @@ -1987,7 +2025,9 @@ def rollback_attach() -> None:
final_failures: list[str] = []
if apply:
final_failures.extend(_portable_failures(read(metadata_path)))
required_resources = ("script/index.yaml", "credentials/credentials.yaml", "AGENTS.md")
required_resources = ["credentials/credentials.yaml", "AGENTS.md"]
if create_script_index:
required_resources.insert(0, "script/index.yaml")
final_failures.extend(
f"WB_CONTROL_PLANE_RESOURCE_MISSING:{relative}"
for relative in required_resources
Expand Down Expand Up @@ -2108,7 +2148,13 @@ def cmd_doctor_workspace(args: list[str], *, command_name: str = "doctor-workspa
missing_required.append(issue)
if parsed.repair and not portable_failures:
try:
result, code = _attach(workspace_root, "none", {}, True)
result, code = _attach(
workspace_root,
"none",
{},
True,
create_script_index=False,
)
except ControlPlaneError as exc:
local_failures.append(exc.code)
result, code = {"status": "issues-found"}, 1
Expand Down
2 changes: 1 addition & 1 deletion scripts/work-bundle/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
RULES = ['repository-boundary', 'lifecycle-authority', 'skill-registry', 'domain-profile', 'doctor-readonly', 'runtime-artifact-format', 'security-exclusion']

CLI_HELP_EPILOG = '''Canonical consolidated command surface:
init-project <project-root> --mode <single-repository|multi-repository>
init-project <project-root> --mode <single-repository|multi-repository> [--workspace-root <workspace-root>]
show-project [--workspace-root <workspace-root> | --project-root <project-root>]
validate-project <project-root> --dry-run
doctor-project <project-root> [--repair] [--force]
Expand Down
Loading