From 3358ae61a36b84a122e3df772fcc5ab35fb6c84a Mon Sep 17 00:00:00 2001 From: Enrico Piovesan Date: Sat, 8 Aug 2026 23:35:10 -0600 Subject: [PATCH 1/2] Fix capability publish for registry PR alignment and relative artifacts. Use registry-approved governing specs in the generated PR body, and canonicalize the artifact path so gh release create works from the registry checkout cwd. Co-authored-by: Cursor --- crates/traverse-cli/src/main.rs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/crates/traverse-cli/src/main.rs b/crates/traverse-cli/src/main.rs index 7cbcd006..b9186ac0 100644 --- a/crates/traverse-cli/src/main.rs +++ b/crates/traverse-cli/src/main.rs @@ -3325,6 +3325,13 @@ fn prepare_and_open_registry_pr( runner: &dyn PublishProcessRunner, registry_repo: &str, ) -> Result)> { + // `gh release create` runs with cwd = registry checkout, so relative artifact + // paths from the Traverse workspace fail with "no matches found". Always pass + // an absolute path when one can be resolved. + let artifact_for_release = request + .artifact_path + .canonicalize() + .unwrap_or_else(|_| request.artifact_path.clone()); run_publish_command( runner, &request.registry_repo_path, @@ -3333,7 +3340,7 @@ fn prepare_and_open_registry_pr( "release".to_string(), "create".to_string(), plan.artifact_release_tag.clone(), - request.artifact_path.display().to_string(), + artifact_for_release.display().to_string(), "--repo".to_string(), registry_repo.to_string(), "--title".to_string(), @@ -3456,8 +3463,10 @@ fn run_publish_command( } fn capability_publish_pr_body(plan: &CapabilityPublishPlan) -> String { + // Governing specs must be from the *registry* approved-spec set, not Traverse's. + // Declaring Traverse-only IDs (056/054/102) fails registry `spec-alignment`. format!( - "## Summary\n\n- publish `{}` version `{}` to the public capability registry\n- add `{}`\n\n## Governing Spec\n\n- `001-registry-foundation`\n- `002-capability-validation`\n- `056-capability-publish`\n- `054-public-scope-registry-ref`\n- `102-contract-surface-coverage`\n\n## Project Item\n\n- Capability publish via traverse-cli\n\n## Validation\n\n- local capability contract validation passed\n- contract surface coverage (action enum ⊆ use_cases) passed\n- use_cases persona_ref resolution against registry personas passed\n- artifact digest computed: `{}`\n- release artifact: `{}`\n", + "## Summary\n\n- publish `{}` version `{}` to the public capability registry\n- add `{}`\n\n## Governing Spec\n\n- `001-registry-foundation`\n- `002-capability-validation`\n- `005-yank-deprecation`\n- `006-public-scope-and-identity`\n- `007-artifact-hosting`\n\n## Project Item\n\n- Capability publish via traverse-cli\n\n## Validation\n\n- local capability contract validation passed\n- contract surface coverage (action enum ⊆ use_cases) passed\n- use_cases persona_ref resolution against registry personas passed\n- artifact digest computed: `{}`\n- release artifact: `{}`\n", plan.capability_id, plan.version, plan.registry_relative_path.display(), @@ -6679,7 +6688,9 @@ mod tests { assert!(commands.contains("gh release create artifacts/traverse-starter.process-1.0.0")); assert!(commands.contains("git checkout -B publish/traverse-starter.process-1.0.0")); assert!(commands.contains("gh pr create")); - assert!(commands.contains("056-capability-publish")); + assert!(commands.contains("001-registry-foundation")); + assert!(commands.contains("007-artifact-hosting")); + assert!(!commands.contains("056-capability-publish")); assert_eq!(json["registry_repo"], "traverse-framework/registry"); assert!(commands.contains("--repo traverse-framework/registry")); } From ef7e1a61b08447be5e0dffd1bde3685b3b4b14d4 Mon Sep 17 00:00:00 2001 From: Enrico Piovesan Date: Sun, 9 Aug 2026 00:58:28 -0600 Subject: [PATCH 2/2] Add Loop core.assign-ownership example for registry publish. Third Loop package capability (WF2 ownership resolver) with ABI-safe WASM, four use-case fixtures, and CI smoke wiring. Co-authored-by: Cursor --- .github/workflows/ci.yml | 3 + .github/workflows/nightly.yml | 3 + examples/core-assign-ownership/DEV-JOURNAL.md | 6 + examples/core-assign-ownership/NOTES.md | 22 + .../artifacts/core-assign-ownership.wasm | Bin 0 -> 12549 bytes .../core-assign-ownership/build-fixture.sh | 42 ++ examples/core-assign-ownership/contract.json | 308 ++++++++++++ examples/core-assign-ownership/manifest.json | 35 ++ .../runtime-requests/uc01-name-match.json | 39 ++ .../runtime-requests/uc02-email-match.json | 39 ++ .../uc03-null-fallback-creator.json | 34 ++ .../uc04-unresolved-fail.json | 34 ++ examples/core-assign-ownership/src/agent.rs | 454 ++++++++++++++++++ .../workflows/assign-ownership/workflow.json | 68 +++ .../ci/core_assign_ownership_example_smoke.sh | 64 +++ scripts/ci/repository_checks.sh | 1 + 16 files changed, 1152 insertions(+) create mode 100644 examples/core-assign-ownership/DEV-JOURNAL.md create mode 100644 examples/core-assign-ownership/NOTES.md create mode 100755 examples/core-assign-ownership/artifacts/core-assign-ownership.wasm create mode 100755 examples/core-assign-ownership/build-fixture.sh create mode 100644 examples/core-assign-ownership/contract.json create mode 100644 examples/core-assign-ownership/manifest.json create mode 100644 examples/core-assign-ownership/runtime-requests/uc01-name-match.json create mode 100644 examples/core-assign-ownership/runtime-requests/uc02-email-match.json create mode 100644 examples/core-assign-ownership/runtime-requests/uc03-null-fallback-creator.json create mode 100644 examples/core-assign-ownership/runtime-requests/uc04-unresolved-fail.json create mode 100644 examples/core-assign-ownership/src/agent.rs create mode 100644 examples/core-assign-ownership/workflows/assign-ownership/workflow.json create mode 100755 scripts/ci/core_assign_ownership_example_smoke.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f02af292..0ce4899d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -93,6 +93,9 @@ jobs: - name: Run core.transition-action-status example smoke run: TRAVERSE_REPO_ROOT="$PWD" bash scripts/ci/core_transition_action_status_example_smoke.sh + - name: Run core.assign-ownership example smoke + run: TRAVERSE_REPO_ROOT="$PWD" bash scripts/ci/core_assign_ownership_example_smoke.sh + - name: Run capability new E2E smoke run: TRAVERSE_REPO_ROOT="$PWD" bash scripts/ci/capability_new_e2e_smoke.sh diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index f8dde9fb..714ee2e6 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -31,6 +31,9 @@ jobs: - name: Run core.transition-action-status example smoke run: TRAVERSE_REPO_ROOT="$PWD" bash scripts/ci/core_transition_action_status_example_smoke.sh + - name: Run core.assign-ownership example smoke + run: TRAVERSE_REPO_ROOT="$PWD" bash scripts/ci/core_assign_ownership_example_smoke.sh + - name: Run capability new E2E smoke run: TRAVERSE_REPO_ROOT="$PWD" bash scripts/ci/capability_new_e2e_smoke.sh diff --git a/examples/core-assign-ownership/DEV-JOURNAL.md b/examples/core-assign-ownership/DEV-JOURNAL.md new file mode 100644 index 00000000..3c120020 --- /dev/null +++ b/examples/core-assign-ownership/DEV-JOURNAL.md @@ -0,0 +1,6 @@ +# core.assign-ownership — dev journal + +- Source: Loop capability package v2 `core.assign-ownership@1.0.0`. +- Guest: `#![no_std]` resolver (id/email/name + fallback). +- Smoke: `bash scripts/ci/core_assign_ownership_example_smoke.sh`. +- Ticket: traverse-framework/traverse#1031. diff --git a/examples/core-assign-ownership/NOTES.md b/examples/core-assign-ownership/NOTES.md new file mode 100644 index 00000000..ed27b117 --- /dev/null +++ b/examples/core-assign-ownership/NOTES.md @@ -0,0 +1,22 @@ +# core.assign-ownership + +Third Loop package capability adapted for registry publication +([#1031](https://github.com/traverse-framework/traverse/issues/1031)). + +Resolves a suggested owner (id / email / name) against workspace members with +configurable fallback (`creator` | `unassigned` | `fail`). + +## Run + +```bash +bash scripts/ci/core_assign_ownership_example_smoke.sh +``` + +## Coverage + +| ID | Fixture | Expected | +|----|---------|----------| +| UC-01 | `uc01-name-match.json` | `ok` / `name_match` → `user-ada` | +| UC-02 | `uc02-email-match.json` | `ok` / `email_match` → `user-bob` | +| UC-03 | `uc03-null-fallback-creator.json` | `ok` / `fallback_creator` → `user-carol` | +| UC-04 | `uc04-unresolved-fail.json` | `unresolved` | diff --git a/examples/core-assign-ownership/artifacts/core-assign-ownership.wasm b/examples/core-assign-ownership/artifacts/core-assign-ownership.wasm new file mode 100755 index 0000000000000000000000000000000000000000..6ef2696d1e062a339ab4c89df9c09988849769cc GIT binary patch literal 12549 zcmds7dyFJUd9SMOndzO!-mHCq;S8>BEM=YexP8pd%-TV^LNU%^A;KR~P)M{lcRP2t zv%9mmGjnUK0J}DK1`Gt}K@<^iUV;z;5#+BV5OG$lL=g@oA|MEM>;w!LA=?QKF~M;8 z{l4m+ncdrSjFi7}*1D^|s`}oKs;@`2`s-RLrGK$}+-z^_?c>3=JVHG9s<+kgh-3c4 zd)|-K@th3QaapL2E0s;9II?9F<3g=Yj3TzZ=WrkT3+h4 zTg&Q73nDDIy|*%GD-&Eb6__y0Q0<E2?ZtC36%A1MEo(R^JA!K)#x2n}2o1 z09Vk#RE#N`UyT(A1t3uXI5CHnayO-`fI10o1;(IM!nC&9J#g2>9aViVnNTGV7hY}f zYLLi*L>yQn3<=YDBD=kl`Her&p>N4F3dqdq|8T>ZmP^UP>m>wsb)=P`<((pe~qNl9gjTz

&tiW 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 G^M3$E=31Bl literal 0 HcmV?d00001 diff --git a/examples/core-assign-ownership/build-fixture.sh b/examples/core-assign-ownership/build-fixture.sh new file mode 100755 index 00000000..cda00ad8 --- /dev/null +++ b/examples/core-assign-ownership/build-fixture.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash + +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +artifact_dir="$script_dir/artifacts" +artifact_path="$artifact_dir/core-assign-ownership.wasm" +manifest_path="$script_dir/manifest.json" + +mkdir -p "$artifact_dir" + +rustup run "$(rustup show active-toolchain | awk '{print $1}')" rustc "$script_dir/src/agent.rs" \ + --target wasm32-unknown-unknown --crate-type cdylib -O -C panic=abort -C strip=symbols \ + --remap-path-prefix "$script_dir=/traverse-repo/agent" -o "$artifact_path" + +digest="$(python3 - "$artifact_path" <<'PY' +import sys +from pathlib import Path + +data = Path(sys.argv[1]).read_bytes() +h = 0xcbf29ce484222325 +for b in data: + h ^= b + h = (h * 0x100000001b3) & 0xFFFFFFFFFFFFFFFF +print(f"fnv1a64:{h:016x}") +PY +)" + +python3 - "$manifest_path" "$digest" <<'PY' +import json +import sys +from pathlib import Path + +manifest = Path(sys.argv[1]) +digest = sys.argv[2] +data = json.loads(manifest.read_text()) +data.setdefault("binary", {})["expected_digest"] = digest +manifest.write_text(json.dumps(data, indent=2) + "\n") +print(f"updated {manifest}: binary.expected_digest={digest}") +PY + +printf 'built %s\n' "$artifact_path" diff --git a/examples/core-assign-ownership/contract.json b/examples/core-assign-ownership/contract.json new file mode 100644 index 00000000..16233e24 --- /dev/null +++ b/examples/core-assign-ownership/contract.json @@ -0,0 +1,308 @@ +{ + "kind": "capability_contract", + "schema_version": "1.0.0", + "id": "core.assign-ownership", + "namespace": "core", + "name": "assign-ownership", + "version": "1.0.0", + "lifecycle": "active", + "owner": { + "team": "loop", + "contact": "founders@loop.dev" + }, + "summary": "Resolves and records clear ownership for an action item (single accountable person).", + "description": "Takes a suggested owner (name, email, or id) plus a workspace member list and returns a canonical owner_id or a structured failure. Supports fallback strategies (creator, unassigned, fail). Pure and deterministic — never mutates storage.\n\nResolution order for a non-null suggestion:\n1. exact member id match\n2. case-insensitive email match\n3. case-insensitive name match\nThen apply `ownership_config.fallback` when unresolved.", + "use_cases": [ + { + "scenario": "As the system, I want a name match to resolve to a canonical user id.", + "input_example": { + "suggested_owner": "Ada Lovelace", + "creator_id": "user-carol", + "workspace_members": [ + { + "id": "user-ada", + "name": "Ada Lovelace", + "email": "ada@loop.dev" + }, + { + "id": "user-bob", + "name": "Bob Smith", + "email": "bob@loop.dev" + } + ], + "ownership_config": { + "version": "1.0", + "fallback": "creator", + "require_active_member": true + } + }, + "output_example": { + "owner_id": "user-ada", + "resolution_method": "name_match", + "reason_code": "ok", + "evaluation_trace": [ + "exact name match on Ada Lovelace" + ] + }, + "happy": true, + "persona_ref": "runtime-engineer" + }, + { + "scenario": "As the system, I want an email suggestion to resolve by email.", + "input_example": { + "suggested_owner": "bob@loop.dev", + "creator_id": "user-carol", + "workspace_members": [ + { + "id": "user-ada", + "name": "Ada Lovelace", + "email": "ada@loop.dev" + }, + { + "id": "user-bob", + "name": "Bob Smith", + "email": "bob@loop.dev" + } + ], + "ownership_config": { + "version": "1.0", + "fallback": "creator", + "require_active_member": true + } + }, + "output_example": { + "owner_id": "user-bob", + "resolution_method": "email_match", + "reason_code": "ok", + "evaluation_trace": [ + "email match on bob@loop.dev" + ] + }, + "happy": true, + "persona_ref": "runtime-engineer" + }, + { + "scenario": "As the system, I want a null suggestion to fall back to the creator.", + "input_example": { + "suggested_owner": null, + "creator_id": "user-carol", + "workspace_members": [ + { + "id": "user-carol", + "name": "Carol Jones", + "email": "carol@loop.dev" + } + ], + "ownership_config": { + "version": "1.0", + "fallback": "creator", + "require_active_member": true + } + }, + "output_example": { + "owner_id": "user-carol", + "resolution_method": "fallback_creator", + "reason_code": "ok", + "evaluation_trace": [ + "suggested_owner null", + "fallback=creator → user-carol" + ] + }, + "happy": true, + "persona_ref": "meeting-organizer" + }, + { + "scenario": "As the system, I want an unknown suggestion to fail closed when fallback is fail.", + "input_example": { + "suggested_owner": "Unknown Person", + "creator_id": "user-carol", + "workspace_members": [ + { + "id": "user-ada", + "name": "Ada Lovelace", + "email": "ada@loop.dev" + } + ], + "ownership_config": { + "version": "1.0", + "fallback": "fail", + "require_active_member": true + } + }, + "output_example": { + "owner_id": null, + "resolution_method": "unresolved", + "reason_code": "unresolved", + "evaluation_trace": [ + "no member match for Unknown Person", + "fallback=fail" + ] + }, + "happy": false, + "persona_ref": "runtime-engineer" + } + ], + "inputs": { + "schema": { + "type": "object", + "required": [ + "suggested_owner", + "workspace_members", + "ownership_config" + ], + "properties": { + "suggested_owner": { + "type": [ + "string", + "null" + ] + }, + "creator_id": { + "type": "string" + }, + "workspace_members": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "email": { + "type": "string" + }, + "active": { + "type": "boolean" + } + } + } + }, + "ownership_config": { + "type": "object", + "required": [ + "version" + ], + "properties": { + "version": { + "type": "string" + }, + "fallback": { + "type": "string", + "enum": [ + "creator", + "unassigned", + "fail" + ] + }, + "require_active_member": { + "type": "boolean" + } + } + } + }, + "additionalProperties": false + } + }, + "outputs": { + "schema": { + "type": "object", + "required": [ + "reason_code" + ], + "properties": { + "owner_id": { + "type": [ + "string", + "null" + ] + }, + "resolution_method": { + "type": "string" + }, + "reason_code": { + "type": "string", + "enum": [ + "ok", + "unresolved", + "inactive_member", + "config_error" + ] + }, + "evaluation_trace": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + }, + "preconditions": [], + "postconditions": [ + { + "id": "deterministic", + "description": "Identical inputs produce identical outputs" + } + ], + "side_effects": [ + { + "kind": "memory_only", + "description": "Pure resolution" + } + ], + "emits": [], + "consumes": [], + "permissions": [ + { + "id": "core.assign-ownership" + } + ], + "execution": { + "binary_format": "wasm", + "entrypoint": { + "kind": "wasi-command", + "command": "run" + }, + "preferred_targets": [ + "local", + "edge", + "browser" + ], + "constraints": { + "host_api_access": "none", + "network_access": "forbidden", + "filesystem_access": "none" + } + }, + "policies": [], + "dependencies": [], + "provenance": { + "source": "ai-assisted", + "author": "loop-founders + traverse-capability-author", + "created_at": "2026-08-08T06:00:00Z", + "spec_ref": "core.assign-ownership@1.0.0", + "adr_refs": [ + "persona-council-review-2026-08-08" + ], + "exception_refs": [] + }, + "evidence": [ + { + "evidence_id": "core-assign-ownership-1.0.0-contract-validation", + "type": "contract_validation", + "status": "passed" + } + ], + "service_type": "stateless", + "permitted_targets": [ + "local", + "edge", + "browser", + "cloud" + ], + "artifact_type": "native" +} diff --git a/examples/core-assign-ownership/manifest.json b/examples/core-assign-ownership/manifest.json new file mode 100644 index 00000000..4dcc20e1 --- /dev/null +++ b/examples/core-assign-ownership/manifest.json @@ -0,0 +1,35 @@ +{ + "kind": "capability_package", + "schema_version": "1.0.0", + "package_id": "core.assign-ownership-agent", + "version": "1.0.0", + "summary": "Capability package for core.assign-ownership@1.0.0 (Loop ownership resolver).", + "capability_ref": { + "id": "core.assign-ownership", + "version": "1.0.0", + "contract_path": "./contract.json" + }, + "workflow_refs": [ + { + "workflow_id": "core.assign-ownership", + "workflow_version": "1.0.0" + } + ], + "source": { + "path": "./src/agent.rs", + "language": "rust", + "entry": "run" + }, + "binary": { + "path": "./artifacts/core-assign-ownership.wasm", + "format": "wasm", + "expected_digest": "fnv1a64:7fabad8a7109a410", + "abi_version": "1.0.0" + }, + "constraints": { + "host_api_access": "none", + "network_access": "forbidden", + "filesystem_access": "none" + }, + "model_dependencies": [] +} diff --git a/examples/core-assign-ownership/runtime-requests/uc01-name-match.json b/examples/core-assign-ownership/runtime-requests/uc01-name-match.json new file mode 100644 index 00000000..160533a9 --- /dev/null +++ b/examples/core-assign-ownership/runtime-requests/uc01-name-match.json @@ -0,0 +1,39 @@ +{ + "kind": "runtime_request", + "schema_version": "1.0.0", + "request_id": "core-assign-ownership-01", + "intent": { + "capability_id": "core.assign-ownership", + "capability_version": "1.0.0" + }, + "input": { + "suggested_owner": "Ada Lovelace", + "creator_id": "user-carol", + "workspace_members": [ + { + "id": "user-ada", + "name": "Ada Lovelace", + "email": "ada@loop.dev" + }, + { + "id": "user-bob", + "name": "Bob Smith", + "email": "bob@loop.dev" + } + ], + "ownership_config": { + "version": "1.0", + "fallback": "creator", + "require_active_member": true + } + }, + "lookup": { + "scope": "prefer_private", + "allow_ambiguity": false + }, + "context": { + "requested_target": "local", + "caller": "core-assign-ownership-smoke" + }, + "governing_spec": "006-runtime-request-execution" +} diff --git a/examples/core-assign-ownership/runtime-requests/uc02-email-match.json b/examples/core-assign-ownership/runtime-requests/uc02-email-match.json new file mode 100644 index 00000000..be55b9d4 --- /dev/null +++ b/examples/core-assign-ownership/runtime-requests/uc02-email-match.json @@ -0,0 +1,39 @@ +{ + "kind": "runtime_request", + "schema_version": "1.0.0", + "request_id": "core-assign-ownership-02", + "intent": { + "capability_id": "core.assign-ownership", + "capability_version": "1.0.0" + }, + "input": { + "suggested_owner": "bob@loop.dev", + "creator_id": "user-carol", + "workspace_members": [ + { + "id": "user-ada", + "name": "Ada Lovelace", + "email": "ada@loop.dev" + }, + { + "id": "user-bob", + "name": "Bob Smith", + "email": "bob@loop.dev" + } + ], + "ownership_config": { + "version": "1.0", + "fallback": "creator", + "require_active_member": true + } + }, + "lookup": { + "scope": "prefer_private", + "allow_ambiguity": false + }, + "context": { + "requested_target": "local", + "caller": "core-assign-ownership-smoke" + }, + "governing_spec": "006-runtime-request-execution" +} diff --git a/examples/core-assign-ownership/runtime-requests/uc03-null-fallback-creator.json b/examples/core-assign-ownership/runtime-requests/uc03-null-fallback-creator.json new file mode 100644 index 00000000..c126d759 --- /dev/null +++ b/examples/core-assign-ownership/runtime-requests/uc03-null-fallback-creator.json @@ -0,0 +1,34 @@ +{ + "kind": "runtime_request", + "schema_version": "1.0.0", + "request_id": "core-assign-ownership-03", + "intent": { + "capability_id": "core.assign-ownership", + "capability_version": "1.0.0" + }, + "input": { + "suggested_owner": null, + "creator_id": "user-carol", + "workspace_members": [ + { + "id": "user-carol", + "name": "Carol Jones", + "email": "carol@loop.dev" + } + ], + "ownership_config": { + "version": "1.0", + "fallback": "creator", + "require_active_member": true + } + }, + "lookup": { + "scope": "prefer_private", + "allow_ambiguity": false + }, + "context": { + "requested_target": "local", + "caller": "core-assign-ownership-smoke" + }, + "governing_spec": "006-runtime-request-execution" +} diff --git a/examples/core-assign-ownership/runtime-requests/uc04-unresolved-fail.json b/examples/core-assign-ownership/runtime-requests/uc04-unresolved-fail.json new file mode 100644 index 00000000..baa4cb5b --- /dev/null +++ b/examples/core-assign-ownership/runtime-requests/uc04-unresolved-fail.json @@ -0,0 +1,34 @@ +{ + "kind": "runtime_request", + "schema_version": "1.0.0", + "request_id": "core-assign-ownership-04", + "intent": { + "capability_id": "core.assign-ownership", + "capability_version": "1.0.0" + }, + "input": { + "suggested_owner": "Unknown Person", + "creator_id": "user-carol", + "workspace_members": [ + { + "id": "user-ada", + "name": "Ada Lovelace", + "email": "ada@loop.dev" + } + ], + "ownership_config": { + "version": "1.0", + "fallback": "fail", + "require_active_member": true + } + }, + "lookup": { + "scope": "prefer_private", + "allow_ambiguity": false + }, + "context": { + "requested_target": "local", + "caller": "core-assign-ownership-smoke" + }, + "governing_spec": "006-runtime-request-execution" +} diff --git a/examples/core-assign-ownership/src/agent.rs b/examples/core-assign-ownership/src/agent.rs new file mode 100644 index 00000000..8fd89407 --- /dev/null +++ b/examples/core-assign-ownership/src/agent.rs @@ -0,0 +1,454 @@ +//! core.assign-ownership — pure owner resolution against workspace members. +#![no_std] +#![no_main] + +#[repr(C)] +struct IoVec { + buffer: *const u8, + length: usize, +} +#[repr(C)] +struct IoVecMut { + buffer: *mut u8, + length: usize, +} + +#[link(wasm_import_module = "wasi_snapshot_preview1")] +unsafe extern "C" { + fn fd_read(fd: u32, vectors: *const IoVecMut, count: usize, read: *mut usize) -> u32; + fn fd_write(fd: u32, vectors: *const IoVec, count: usize, written: *mut usize) -> u32; +} + +static mut INPUT_BUF: [u8; 12288] = [0; 12288]; +static mut OUTPUT_BUF: [u8; 4096] = [0; 4096]; + +const MAX_TRACE: usize = 4; + +#[unsafe(no_mangle)] +pub extern "C" fn _start() { + unsafe { + let mut total = 0usize; + loop { + let vec = IoVecMut { + buffer: INPUT_BUF.as_mut_ptr().add(total), + length: INPUT_BUF.len() - total, + }; + let mut n = 0usize; + if fd_read(0, &vec, 1, &mut n) != 0 || n == 0 { + break; + } + total += n; + if total >= INPUT_BUF.len() { + break; + } + } + let out_len = evaluate(&INPUT_BUF[..total], &mut OUTPUT_BUF); + let out = IoVec { + buffer: OUTPUT_BUF.as_ptr(), + length: out_len, + }; + let mut written = 0usize; + let _ = fd_write(1, &out, 1, &mut written); + } +} + +unsafe fn evaluate(input: &[u8], out: &mut [u8]) -> usize { + let members = array_after_key(input, b"\"workspace_members\"").unwrap_or(b"[]"); + let config = object_after_key(input, b"\"ownership_config\"").unwrap_or(b""); + let creator = extract_string_at_depth(input, b"\"creator_id\"", 1); + let suggested_null = is_null_at_depth(input, b"\"suggested_owner\"", 1); + let suggested = if suggested_null { + b"" + } else { + extract_string_at_depth(input, b"\"suggested_owner\"", 1) + }; + + if config.is_empty() { + return write_result( + out, + None, + b"config_error", + b"config_error", + &[b"ownership_config missing"], + ); + } + + let fallback = extract_string(config, b"\"fallback\""); + let fallback = if fallback.is_empty() { + b"creator" + } else { + fallback + }; + if !matches!(fallback, b"creator" | b"unassigned" | b"fail") { + return write_result( + out, + None, + b"config_error", + b"config_error", + &[b"invalid fallback"], + ); + } + let require_active = extract_bool(config, b"\"require_active_member\"").unwrap_or(true); + + let mut traces: [&[u8]; MAX_TRACE] = [b""; MAX_TRACE]; + let mut trace_n = 0usize; + + if suggested_null || suggested.is_empty() { + traces[trace_n] = b"suggested_owner null"; + trace_n += 1; + return apply_fallback(out, fallback, creator, members, require_active, &mut traces, trace_n); + } + + if let Some((id, method, note)) = resolve_member(members, suggested, require_active) { + if method == b"inactive" { + traces[0] = note; + return write_result(out, None, b"unresolved", b"inactive_member", &traces[..1]); + } + traces[0] = note; + return write_result(out, Some(id), method, b"ok", &traces[..1]); + } + + traces[0] = b"no member match for suggestion"; + trace_n = 1; + apply_fallback(out, fallback, creator, members, require_active, &mut traces, trace_n) +} + +fn apply_fallback<'a>( + out: &mut [u8], + fallback: &[u8], + creator: &'a [u8], + members: &[u8], + require_active: bool, + traces: &mut [&'a [u8]; MAX_TRACE], + mut trace_n: usize, +) -> usize { + match fallback { + b"creator" => { + if creator.is_empty() { + if trace_n < MAX_TRACE { + traces[trace_n] = b"fallback=creator but creator_id missing"; + trace_n += 1; + } + return write_result(out, None, b"unresolved", b"unresolved", &traces[..trace_n]); + } + if require_active { + if let Some((id, method, _)) = resolve_member(members, creator, true) { + if method != b"inactive" { + if trace_n < MAX_TRACE { + traces[trace_n] = b"fallback=creator"; + trace_n += 1; + } + return write_result( + out, + Some(id), + b"fallback_creator", + b"ok", + &traces[..trace_n], + ); + } + } + // Creator id may not be listed; still accept when present as string id. + } + if trace_n < MAX_TRACE { + traces[trace_n] = b"fallback=creator"; + trace_n += 1; + } + write_result( + out, + Some(creator), + b"fallback_creator", + b"ok", + &traces[..trace_n], + ) + } + b"unassigned" => { + if trace_n < MAX_TRACE { + traces[trace_n] = b"fallback=unassigned"; + trace_n += 1; + } + write_result( + out, + None, + b"fallback_unassigned", + b"ok", + &traces[..trace_n], + ) + } + _ => { + if trace_n < MAX_TRACE { + traces[trace_n] = b"fallback=fail"; + trace_n += 1; + } + write_result(out, None, b"unresolved", b"unresolved", &traces[..trace_n]) + } + } +} + +/// Returns (id, method, note). method == b"inactive" means matched but inactive. +fn resolve_member<'a>( + members: &'a [u8], + suggestion: &[u8], + require_active: bool, +) -> Option<(&'a [u8], &'static [u8], &'static [u8])> { + let mut rest = members; + if rest.first() == Some(&b'[') { + rest = &rest[1..]; + } + loop { + while rest + .first() + .is_some_and(|b| matches!(*b, b' ' | b'\n' | b'\t' | b',')) + { + rest = &rest[1..]; + } + if rest.is_empty() || rest.first() == Some(&b']') { + return None; + } + if rest.first() != Some(&b'{') { + return None; + } + let end = balanced_end(rest, b'{', b'}')?; + let obj = &rest[..=end]; + let id = extract_string(obj, b"\"id\""); + let name = extract_string(obj, b"\"name\""); + let email = extract_string(obj, b"\"email\""); + let active = extract_bool(obj, b"\"active\"").unwrap_or(true); + + let mut method: Option<&'static [u8]> = None; + let mut note: Option<&'static [u8]> = None; + if !id.is_empty() && eq_ascii_ci(id, suggestion) { + method = Some(b"id_match"); + note = Some(b"id match"); + } else if !email.is_empty() && eq_ascii_ci(email, suggestion) { + method = Some(b"email_match"); + note = Some(b"email match"); + } else if !name.is_empty() && eq_ascii_ci(name, suggestion) { + method = Some(b"name_match"); + note = Some(b"exact name match"); + } + + if let (Some(m), Some(n)) = (method, note) { + if require_active && !active { + return Some((id, b"inactive", b"matched inactive member")); + } + return Some((id, m, n)); + } + rest = &rest[end + 1..]; + } +} + +fn write_result( + out: &mut [u8], + owner_id: Option<&[u8]>, + method: &[u8], + reason: &[u8], + traces: &[&[u8]], +) -> usize { + let mut i = 0usize; + i = copy(out, i, b"{\"owner_id\":"); + if let Some(id) = owner_id { + i = copy(out, i, b"\""); + i = copy(out, i, id); + i = copy(out, i, b"\""); + } else { + i = copy(out, i, b"null"); + } + i = copy(out, i, b",\"resolution_method\":\""); + i = copy(out, i, method); + i = copy(out, i, b"\",\"reason_code\":\""); + i = copy(out, i, reason); + i = copy(out, i, b"\",\"evaluation_trace\":["); + for (idx, t) in traces.iter().enumerate() { + if idx > 0 { + i = copy(out, i, b","); + } + i = copy(out, i, b"\""); + i = copy(out, i, t); + i = copy(out, i, b"\""); + } + i = copy(out, i, b"]}"); + i +} + +fn eq_ascii_ci(a: &[u8], b: &[u8]) -> bool { + if a.len() != b.len() { + return false; + } + a.iter() + .zip(b.iter()) + .all(|(x, y)| x.to_ascii_lowercase() == y.to_ascii_lowercase()) +} + +fn is_null_at_depth(hay: &[u8], key: &[u8], depth: i32) -> bool { + let Some(pos) = find_key_at_depth(hay, key, depth) else { + return false; + }; + let after = &hay[pos + key.len()..]; + let Some(colon) = after.iter().position(|b| *b == b':') else { + return false; + }; + let rest = skip_ws(&after[colon + 1..]); + rest.starts_with(b"null") +} + +fn object_after_key<'a>(hay: &'a [u8], key: &[u8]) -> Option<&'a [u8]> { + let pos = find_key_at_depth(hay, key, 1)?; + let after = &hay[pos + key.len()..]; + let colon = after.iter().position(|b| *b == b':')?; + let rest = skip_ws(&after[colon + 1..]); + if rest.first() != Some(&b'{') { + return None; + } + let end = balanced_end(rest, b'{', b'}')?; + Some(&rest[..=end]) +} + +fn array_after_key<'a>(hay: &'a [u8], key: &[u8]) -> Option<&'a [u8]> { + let pos = find_key_at_depth(hay, key, 1)?; + let after = &hay[pos + key.len()..]; + let colon = after.iter().position(|b| *b == b':')?; + let rest = skip_ws(&after[colon + 1..]); + if rest.first() != Some(&b'[') { + return None; + } + let end = balanced_end(rest, b'[', b']')?; + Some(&rest[..=end]) +} + +fn skip_ws(s: &[u8]) -> &[u8] { + let mut rest = s; + while rest + .first() + .is_some_and(|b| matches!(*b, b' ' | b'\n' | b'\t' | b'\r')) + { + rest = &rest[1..]; + } + rest +} + +fn balanced_end(s: &[u8], open: u8, close: u8) -> Option { + let mut depth = 0i32; + let mut in_str = false; + let mut i = 0usize; + while i < s.len() { + let b = s[i]; + if in_str { + if b == b'\\' { + i += 2; + continue; + } + if b == b'"' { + in_str = false; + } + i += 1; + continue; + } + match b { + b'"' => in_str = true, + x if x == open => depth += 1, + x if x == close => { + depth -= 1; + if depth == 0 { + return Some(i); + } + } + _ => {} + } + i += 1; + } + None +} + +fn extract_string<'a>(hay: &'a [u8], key: &[u8]) -> &'a [u8] { + let Some(pos) = find(hay, key) else { + return b""; + }; + string_value_after(&hay[pos + key.len()..]) +} + +fn extract_string_at_depth<'a>(hay: &'a [u8], key: &[u8], depth: i32) -> &'a [u8] { + let Some(pos) = find_key_at_depth(hay, key, depth) else { + return b""; + }; + string_value_after(&hay[pos + key.len()..]) +} + +fn find_key_at_depth(hay: &[u8], key: &[u8], target_depth: i32) -> Option { + let mut depth = 0i32; + let mut in_str = false; + let mut i = 0usize; + while i + key.len() <= hay.len() { + let b = hay[i]; + if in_str { + if b == b'\\' { + i += 2; + continue; + } + if b == b'"' { + in_str = false; + } + i += 1; + continue; + } + match b { + b'"' => { + if depth == target_depth && hay[i..].starts_with(key) { + return Some(i); + } + in_str = true; + } + b'{' | b'[' => depth += 1, + b'}' | b']' => depth -= 1, + _ => {} + } + i += 1; + } + None +} + +fn string_value_after<'a>(after_key: &'a [u8]) -> &'a [u8] { + let Some(colon) = after_key.iter().position(|b| *b == b':') else { + return b""; + }; + let mut rest = skip_ws(&after_key[colon + 1..]); + if rest.first() != Some(&b'"') { + return b""; + } + rest = &rest[1..]; + let Some(end) = rest.iter().position(|b| *b == b'"') else { + return b""; + }; + &rest[..end] +} + +fn extract_bool(hay: &[u8], key: &[u8]) -> Option { + let pos = find(hay, key)?; + let after = &hay[pos + key.len()..]; + let colon = after.iter().position(|b| *b == b':')?; + let rest = skip_ws(&after[colon + 1..]); + if rest.starts_with(b"true") { + Some(true) + } else if rest.starts_with(b"false") { + Some(false) + } else { + None + } +} + +fn copy(out: &mut [u8], at: usize, bytes: &[u8]) -> usize { + let end = at + bytes.len(); + if end > out.len() { + return at; + } + out[at..end].copy_from_slice(bytes); + end +} + +fn find(hay: &[u8], needle: &[u8]) -> Option { + hay.windows(needle.len()).position(|w| w == needle) +} + +#[panic_handler] +fn panic(_: &core::panic::PanicInfo<'_>) -> ! { + loop {} +} diff --git a/examples/core-assign-ownership/workflows/assign-ownership/workflow.json b/examples/core-assign-ownership/workflows/assign-ownership/workflow.json new file mode 100644 index 00000000..34639d39 --- /dev/null +++ b/examples/core-assign-ownership/workflows/assign-ownership/workflow.json @@ -0,0 +1,68 @@ +{ + "kind": "workflow_definition", + "schema_version": "1.0.0", + "id": "core.assign-ownership", + "name": "assign-ownership", + "version": "1.0.0", + "lifecycle": "active", + "owner": { + "team": "loop", + "contact": "founders@loop.dev" + }, + "summary": "Run core.assign-ownership as one governed workflow.", + "inputs": { + "schema": { + "type": "object", + "required": [ + "suggested_owner", + "workspace_members", + "ownership_config" + ], + "additionalProperties": true + } + }, + "outputs": { + "schema": { + "type": "object", + "required": [ + "reason_code" + ], + "additionalProperties": true + } + }, + "nodes": [ + { + "node_id": "assign", + "capability_id": "core.assign-ownership", + "capability_version": "1.0.0", + "input": { + "from_workflow_input": [ + "suggested_owner", + "creator_id", + "workspace_members", + "ownership_config" + ] + }, + "output": { + "to_workflow_state": [ + "owner_id", + "resolution_method", + "reason_code", + "evaluation_trace" + ] + } + } + ], + "edges": [], + "start_node": "assign", + "terminal_nodes": [ + "assign" + ], + "tags": [ + "loop", + "action-items", + "ownership", + "core" + ], + "governing_spec": "007-workflow-registry-traversal" +} diff --git a/scripts/ci/core_assign_ownership_example_smoke.sh b/scripts/ci/core_assign_ownership_example_smoke.sh new file mode 100755 index 00000000..4a88ec41 --- /dev/null +++ b/scripts/ci/core_assign_ownership_example_smoke.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# End-to-end smoke for examples/core-assign-ownership. + +set -euo pipefail + +repo_root="${TRAVERSE_REPO_ROOT:-$(pwd)}" +cd "$repo_root" + +pkg="examples/core-assign-ownership" +cli=(cargo run -q -p traverse-cli-rs --) + +fail() { + printf 'FAIL: %s\n' "$1" >&2 + exit 1 +} + +require_match() { + local haystack="$1" + local needle="$2" + local label="$3" + grep -q "$needle" <<<"$haystack" || fail "$label (missing: $needle)" +} + +echo "==> build-fixture" +bash "$pkg/build-fixture.sh" + +echo "==> capability inspect" +contract_out="$("${cli[@]}" capability inspect "$pkg/contract.json")" +printf '%s\n' "$contract_out" +require_match "$contract_out" "id: core.assign-ownership" "contract inspect id" + +echo "==> wasm abi verify" +abi_out="$("${cli[@]}" wasm abi verify "$pkg/artifacts/core-assign-ownership.wasm")" +printf '%s\n' "$abi_out" +require_match "$abi_out" "import whitelist passed" "abi verify" + +echo "==> capability-package inspect" +pkg_out="$("${cli[@]}" capability-package inspect "$pkg/manifest.json")" +printf '%s\n' "$pkg_out" +require_match "$pkg_out" "package_id: core.assign-ownership-agent" "package_id" + +assert_execute() { + local request="$1" + local code="$2" + local label="$3" + local extra="${4:-}" + + echo "==> execute $label" + local out + out="$("${cli[@]}" capability-package execute "$pkg/manifest.json" "$request")" + printf '%s\n' "$out" + require_match "$out" "status: completed" "$label status" + require_match "$out" "\"reason_code\": \"$code\"" "$label reason_code" + if [[ -n "$extra" ]]; then + require_match "$out" "$extra" "$label extra" + fi +} + +assert_execute "$pkg/runtime-requests/uc01-name-match.json" "ok" "UC-01" '"owner_id": "user-ada"' +assert_execute "$pkg/runtime-requests/uc02-email-match.json" "ok" "UC-02" '"owner_id": "user-bob"' +assert_execute "$pkg/runtime-requests/uc03-null-fallback-creator.json" "ok" "UC-03" '"owner_id": "user-carol"' +assert_execute "$pkg/runtime-requests/uc04-unresolved-fail.json" "unresolved" "UC-04" '"owner_id": null' + +echo "OK: core.assign-ownership 1.0.0 E2E smoke passed" diff --git a/scripts/ci/repository_checks.sh b/scripts/ci/repository_checks.sh index 2bac7b0a..2d4b53ab 100644 --- a/scripts/ci/repository_checks.sh +++ b/scripts/ci/repository_checks.sh @@ -128,6 +128,7 @@ required_files=( "scripts/ci/core_authorize_example_smoke.sh" "scripts/ci/core_process_comment_example_smoke.sh" "scripts/ci/core_transition_action_status_example_smoke.sh" + "scripts/ci/core_assign_ownership_example_smoke.sh" "scripts/ci/capability_new_e2e_smoke.sh" "scripts/ci/wasi_host_abi_imports.sh" "scripts/ci/zero_to_hero_acceptance.sh"