Metadata
| Status | done |
|---|---|
| Assigned | agent-2375 |
| Agent identity | 3184716484e6f0ea08bb13539daf07686ee79d440505f1fdf2de0357707034c3 |
| Model | claude:opus |
| Created | 2026-05-04T17:38:22.609744571+00:00 |
| Started | 2026-05-04T18:38:52.552596226+00:00 |
| Completed | 2026-05-04T18:44:53.170823616+00:00 |
| Tags | agency,sync,research,federation, eval-scheduled |
| Eval score | 0.91 |
| └ blocking impact | 0.94 |
| └ completeness | 0.94 |
| └ coordination overhead | 0.88 |
| └ correctness | 0.92 |
| └ downstream usability | 0.93 |
| └ efficiency | 0.90 |
| └ intent fidelity | 0.90 |
| └ style adherence | 0.95 |
Description
Description
Agency distributes primitives via a 12-column starter CSV (primitives/starter.csv, columns per https://github.com/agentbureau/agency agency-primitive-extraction skill: type, name, description, quality, domain_specificity, domain, origin_instance_id, parent_content_hash, scope, parent_ids, generation, created_by). Workgraph stores per-primitive YAML files under .wg/agency/primitives/{components,outcomes,tradeoffs}/{hash}.yaml. Workgraph already has wg agency import (src/commands/agency_import.rs) and wg agency push/pull — investigate whether they accept agency starter CSV format and round-trip cleanly.
Specific things to investigate
- Read
src/commands/agency_import.rsend-to-end. What format does it accept today? - Does it understand: type discriminator column, quality, domain_specificity, domain (comma-separated), parent_ids (JSON list), generation, created_by enum?
- Is there an export path?
src/commands/agency_push.rs— does it emit CSV? - For each missing field on import, propose: defaults vs error vs ignore.
- Verify
import-manifest.yaml(saw at.wg/agency/import-manifest.yaml) — what does it track? - Round-trip test: take agency's starter.csv, import into wg, export, byte-diff against original. What changes? Why?
Deliverable
wg log entry with:
- Current
wg agency importCSV schema (cite agency_import.rs line ranges) - Diff vs agency 12-col schema
- Concrete patch list: add columns, add parsers, add export path
- Round-trip test plan (what files/fixtures, what equality predicate — byte-equal vs canonical-equal)
- Note whether import is dependent on the hash-compat decision from research-agency-hash-compat (it almost certainly is)
Validation
- Cites every line range in src/commands/agency_import.rs that handles a CSV column
- Each of the 12 agency columns mapped to: handled / partial / missing in wg
-
Round-trip test fixture path proposed (
tests/fixtures/agency-starter.csvor similar) - Documents the dependency on hash-compat (this work is gated on that decision)
Depends on
Required by
- (none)
Log
- 2026-05-04T17:38:22.530557658+00:00 Task paused
- 2026-05-04T18:37:42.133667675+00:00 Task published
- 2026-05-04T18:38:52.552601145+00:00 Spawned by coordinator --executor claude --model opus
- 2026-05-04T18:39:06.709769172+00:00 Starting research: agency CSV roundtrip — reading agency_import.rs end-to-end and mapping 12-col agency schema
- 2026-05-04T18:41:09.937697713+00:00 Investigation done. agency_import.rs reads 9-col Agency format (cols 0-8); cols 9-11 (parent_ids/generation/created_by) of upstream 12-col schema are silently dropped. agency_push.rs is YAML-to-YAML federation transfer — no CSV writer exists anywhere in src/. Round-trip CANNOT be byte-equal: workgraph's content_hash is wider than agency's (description-only) and Lineage.created_by is overwritten with provenance_tag during import. Writing structured deliverable next.
- 2026-05-04T18:43:19.999273033+00:00 === RESEARCH: agency CSV roundtrip — starter import/export === ## A. Current `wg agency import` CSV schema (citing src/commands/agency_import.rs) ### A.1 Format detection (lines 441-463) - `enum CsvFormat { Legacy, Agency }` — Legacy = 7 cols, "Agency" = 9 cols - `detect_format` (450-463): if `headers.len() >= 9` → Agency; else fall back to header-name probe of col 3 (`quality`/`domain_specificity`); else Legacy - IMPORTANT: a 12-col upstream file matches `>= 9` and is parsed AS AGENCY, but cols 9-11 are never read (see A.3) ### A.2 Import row pipeline (lines 122-337, `run_from_bytes`) - 136-139: build `csv::Reader` (default `flexible=false` — every row must match header field count) - 146-156: per-row dispatch reads ptype/name/description from cols 0/1/2 unconditionally; then branches on `format` → `parse_agency_columns` or `parse_legacy_columns` - 158-170: builds `Lineage { parent_ids: [parent_content_hash] if non-empty, generation: 0, created_by: format!("{}-v{}", provenance_tag, CARGO_PKG_VERSION), created_at: Utc::now() }`. NB: `created_by` is overwritten regardless of CSV input, and `generation` is hard-coded 0. - 172-181: builds `AccessControl { owner: provenance_tag, policy: Open }` and `PerformanceRecord { task_count: 0, avg_score: quality_score, evaluations: [] }` - 183-188: `normalized_type` accepts both Agency vocab (`role_component`/`desired_outcome`/`trade_off_config`) and legacy aliases (`skill`/`outcome`/`tradeoff`) - 191-219 (component): hash via `content_hash_component(description, ComponentCategory::Translated, ContentRef::Inline(description))` — wider than agency's `SHA256(description)` - 220-256 (outcome): in Agency mode `success_criteria = []` (legacy splits col4 by '\n'). Hash via `content_hash_outcome(description, success_criteria)` — wider than agency - 257-301 (tradeoff): in Agency mode `acceptable=[]`, `unacceptable=[]` (legacy splits cols 3/4 by ','). Hash via `content_hash_tradeoff(acceptable, unacceptable, description)` — wider than agency - 302-313: unknown ptype → log + `skipped` count ### A.3 Agency-mode column parser (lines 488-555, `parse_agency_columns`) | col | name | parsed at lines | sink | |----:|-----------------------|-----------------|------| | 0 | type | 149 | dispatch (component/outcome/tradeoff) | | 1 | name | 150 | primitive.name | | 2 | description | 151 | primitive.description (and hash input) | | 3 | quality | 501-504 | divided by 100 → `performance.avg_score` (Option<f64>) | | 4 | domain_specificity | 507-510 | `metadata["domain_specificity"]` (string) | | 5 | domain | 513-521 | `domain_tags: Vec<String>` (comma-split) | | 6 | origin_instance_id | 524-527 | `metadata["origin_instance_id"]` | | 7 | parent_content_hash | 530 | `metadata["parent_content_hash"]` AND prepended to `lineage.parent_ids` | | 8 | scope | 533-536 | `metadata["scope"]` | ### A.4 Manifest (lines 22-73, 110-119) `.wg/agency/import-manifest.yaml` tracks `{source, version (CARGO_PKG_VERSION), imported_at (RFC3339), counts: {role_components, desired_outcomes, trade_off_configs}, content_hash (SHA256 of CSV bytes)}`. Used for `wg agency import --check` (exit 0=changed, 1=up-to-date, 2=fetch-failed) and idempotent `--upstream` skips. ## B. Mapping vs agency v1.2.4 12-col schema Per design-agency-sync delta map (and confirmed by reading `./agency/starter.csv`, which is 9-col — workgraph ships the legacy subset): | # | agency col | wg import status | notes | |---:|---------------------|------------------|-------| | 0 | type | **handled** | Both vocabs accepted (lines 183-188) | | 1 | name | **handled** | line 150 | | 2 | description | **handled** | line 151 | | 3 | quality | **partial** | int 0-100 → f64 0..1 lossy (line 503: `v / 100.0`); originals 100, 0, 60 round-trip but 65 → 0.65 → 65 OK; non-integer agency values would round | | 4 | domain_specificity | **partial** | Stored as string in metadata, not a typed numeric or scalar field | | 5 | domain | **handled** | comma-split into `domain_tags` (lines 513-521) | | 6 | origin_instance_id | **partial** | Stored as metadata string only — never reused for federation identity | | 7 | parent_content_hash | **partial** | Stored both as metadata AND lineage.parent_ids[0] (lines 158-163, 548-552). Different agency hash space → meaningless until hash-compat decided | | 8 | scope | **partial** | Stored as metadata string. Not yet wired to functional-agent dispatch (see research-agency-scope-rules) | | 9 | parent_ids (JSON list) | **MISSING** | parse_agency_columns never reads col 9. JSON parse not implemented | | 10 | generation | **MISSING** | Hard-coded `generation: 0` (line 167); col 10 never read | | 11 | created_by (enum) | **MISSING** | Overwritten with `format!("{}-v{}", provenance_tag, CARGO_PKG_VERSION)` at line 168; col 11 never read | Additionally **MISSING globally**: - **No export path.** `wg agency export` does not exist (`wg agency --help` confirmed). `agency_push.rs` (lines 41-114) only operates on YAML-to-YAML via `federation::transfer`. There is no `csv::Writer` in `src/` (grep confirmed). - **`reframing_potential`** field present in agency v1.2.4 spec but absent from both starter CSV and wg primitives. ## C. Concrete patch list ### C.1 Import-side (src/commands/agency_import.rs) 1. **Read cols 9-11**. Extend `parse_agency_columns` (488-555) with three optional reads: - col 9: `parent_ids` — try `serde_json::from_str::<Vec<String>>` first; fall back to comma-split for non-JSON values; merge with `parent_content_hash` (col 7) preserving order - col 10: `generation` — `record.get(10).and_then(|s| s.trim().parse::<u32>().ok()).unwrap_or(0)` - col 11: `created_by` — accept the agency enum verbatim (`human|import|evolver|agent_creator`); only fall back to provenance-tag synthesis if col 11 empty 2. **Lineage construction** (158-170): replace hard-coded `generation: 0` and `created_by: format!(...)` with values from CSV (with provenance fallback). Provenance survives in `metadata["provenance_tag"]` so audit trail is preserved. 3. **Detect 12-col format** (450-463): change branch from `>= 9` to a tri-state `{Legacy, Agency9, Agency12}`; or simpler — keep `Agency` and treat cols 9-11 as optional with `record.get(N)` returning None for 9-col files. The latter is preferred — additive, no enum churn. 4. **Bumped manifest version** semantic: include a `schema: agency-12col-v1` field so re-imports detect format upgrades and rewrite primitives with new lineage. ### C.2 Export-side (NEW: src/commands/agency_export.rs + CLI wiring) 1. New subcommand `wg agency export <out.csv>` writing the 12-col format (default to `--format agency12`). 2. Walk `.wg/agency/primitives/{components,outcomes,tradeoffs}/*.yaml` via `LocalStore::list_*`. 3. Emit each row: - col 0 type: reverse map (`component → role_component`, `outcome → desired_outcome`, `tradeoff → trade_off_config`) - col 1-2: name, description verbatim - col 3 quality: `(performance.avg_score.unwrap_or(1.0) * 100.0).round() as u32` - col 4: `metadata["domain_specificity"].unwrap_or("")` - col 5: domain_tags joined by `,` - col 6: `metadata["origin_instance_id"].unwrap_or("")` - col 7: `metadata["parent_content_hash"].unwrap_or("")` (or first lineage.parent_id if present and we trust it) - col 8: `metadata["scope"].unwrap_or("")` - col 9: `serde_json::to_string(&lineage.parent_ids)` (if non-empty) - col 10: `lineage.generation.to_string()` - col 11: `lineage.created_by` (passed through verbatim — agency rejects unknown enum values, so on round-trip from wg-evolver we either map `evolver-{id}` → `evolver` and lose the run_id, or accept that wg's wider created_by leaks into agency as a string) 4. CLI flag `--filter origin_instance_id=...` to round-trip only entities sourced from a specific instance. 5. Stable row ordering: sort by (type, name, hash) so two exports of an unchanged store produce byte-equal output. ### C.3 Cross-cutting (depends on hash-compat decision) - If hash-compat (`research-agency-hash-compat`) decides wg adopts agency's narrower hash inputs, then the `id` field on YAML primitives matches agency's `content_hash`, and `parent_ids` from col 9 can be looked up locally (federation works). - If hash-compat decides workgraph keeps its wider hash and translates at the boundary, export must produce two ID columns or maintain a `metadata["agency_content_hash"]` field that gets emitted as the `parent_content_hash` (col 7) and re-used as join key. ### C.4 Field ergonomics (small) - Lossy `quality/100` (line 503) drops sub-percent precision. Either (a) preserve `metadata["agency_quality"]` as the original int string for byte-perfect round-trip, or (b) accept lossy and document. (b) is fine if hash-compat lands first since downstream agency consumers re-run evaluation anyway. - `domain_tags` exporter must escape commas inside tags by quoting the whole CSV cell — `csv::Writer` does this automatically; just avoid manual `join(",")` to a raw string buffer. ## D. Round-trip test plan ### D.1 Fixtures Add **two** fixtures under `tests/fixtures/agency/`: 1. `tests/fixtures/agency/starter-9col.csv` — copy of current `./agency/starter.csv` (the bundled subset). Already 9-col. 2. `tests/fixtures/agency/starter-12col.csv` — small (~5 row) hand-authored fixture covering each primitive type and exercising: - non-empty `parent_ids` JSON list (col 9): `["hash1","hash2"]` - non-zero generation (col 10): `2` - each `created_by` enum value (col 11): one row each of `human`, `import`, `evolver`, `agent_creator` - a comma-containing domain tag (`"analysis,review"`) to exercise CSV quoting - a row with empty col 7 + populated col 9 (parent_content_hash empty but parent_ids non-empty) to verify col 9 is read independently A future task should pull the actual upstream `https://raw.githubusercontent.com/agentbureau/agency/main/agency/starter.csv` and freeze it as `tests/fixtures/agency/starter-upstream-v1.2.4.csv` for full-volume drift detection — but that's a snapshot test owned by the integration task, not this research. ### D.2 Equality predicates (two levels — not byte-equal) **Level 1 — canonical-equal (target for the impl task):** - Sort rows by (type, name) before diffing (column ordering canonical). - Whitespace inside CSV cells preserved verbatim. - Numeric `quality` preserved as-written when input is integer 0-100 (use `metadata["agency_quality"]` for round-trip fidelity, OR document precision-loss as acceptable). - `parent_ids` JSON list re-serialized: assert deserialize-equal not byte-equal (member order may differ if we sort for determinism). - Use a Rust test that imports → exports → re-imports → asserts second-import produces same set of `id`s as first. **Level 2 — byte-equal (nice-to-have, gated on hash-compat):** - Possible only after `research-agency-hash-compat` decides wg's content_hash matches agency's. - Use `csv::Writer` with explicit `terminator(b'\n')`, `quote_style(QuoteStyle::Necessary)`, no BOM. - Test: `assert_eq!(input_bytes, exported_bytes)` for the upstream snapshot fixture after sorting input by canonical key first (because upstream ordering is not guaranteed deterministic). ### D.3 Test cases (proposed — for impl-agency-csv-roundtrip) - `test_import_12col_reads_parent_ids` — col 9 JSON list landed in lineage.parent_ids - `test_import_12col_reads_generation` — col 10 lands in lineage.generation - `test_import_12col_reads_created_by_enum` — col 11 verbatim - `test_import_9col_still_works` — no regression on bundled starter.csv - `test_export_12col_emits_all_columns` — write known fixture, read exported CSV, check headers + 12 cols populated - `test_roundtrip_canonical_equal` — import → export → import → identical primitive set (by `id`) - `test_roundtrip_byte_equal_after_canonical_sort` — feature-gated until hash-compat lands - `test_quality_precision_metadata_preserved` — `metadata["agency_quality"]` = "65" survives round-trip ### D.4 Smoke gate hookup Add scenario `tests/smoke/scenarios/agency_csv_roundtrip.sh` (impl task creates this, listed in `owners` of `manifest.toml`): ``` wg agency import tests/fixtures/agency/starter-12col.csv --tag smoke wg agency export /tmp/wg-export.csv diff <(sort tests/fixtures/agency/starter-12col.csv) <(sort /tmp/wg-export.csv) ``` ## E. Hash-compat dependency This work is **gated on `research-agency-hash-compat`** for the byte-equality goal but NOT for additive 12-col import. Specifically: | Goal | Gated on hash-compat? | |---------------------------------------------------|----------------------| | Read cols 9-11 (additive parsing) | NO — independent | | Preserve agency lineage on import | NO — independent | | Emit valid 12-col CSV | NO — independent | | Federation: parent_ids resolve locally | **YES** | | Byte-equal round-trip with upstream starter.csv | **YES** | | `parent_content_hash` becomes a real foreign key | **YES** | Concretely: until hash-compat decides workgraph either (a) narrows its hash inputs or (b) maintains a translation table, the values in `parent_ids` (col 9) and `parent_content_hash` (col 7) reference an ID space that doesn't exist locally. They round-trip as opaque strings, which is fine for additive import/export but blocks any meaningful join to local primitives. Recommended implementation order for the impl phase: 1. impl-agency-hash-compat lands first (decision committed in code constants/translation) 2. impl-agency-csv-roundtrip lands second, consuming whatever ID space hash-compat chose 3. integration phase wires `agency_bridge` field-mapping using both If impl-agency-csv-roundtrip is asked to land first (per the parallel impl plan in design-agency-sync §B), it should: - Implement additive 12-col read + 12-col export - Treat parent_ids/parent_content_hash as opaque strings for now - Add a TODO with a citation to research-agency-hash-compat for the federation join step - Land the smoke scenario at `Level 1` (canonical-equal); upgrade to `Level 2` (byte-equal) in a follow-up patch after hash-compat ships ## F. Summary - Today: `wg agency import` reads agency's 9-col legacy form fine but **silently truncates the upstream 12-col form** (cols 9-11 dropped). No `wg agency export` exists — round-trip is currently impossible in either direction. Patch list above is small (~150 LOC for import, ~250 LOC for export including CLI wiring + tests). Byte-equal round-trip is gated on hash-compat; canonical-equal is achievable independently.
- 2026-05-04T18:44:17.558490705+00:00 Validated: (1) agency_import.rs line ranges cited per column [149,150,151,501-504,507-510,513-521,524-527,530,533-536] plus structural ranges [22-73,122-337,441-463,488-555]; (2) 12-col table with handled/partial/missing per col; (3) fixtures proposed at tests/fixtures/agency/starter-{9col,12col}.csv; (4) hash-compat dependency documented with gated/independent table + recommended impl order. Research deliverable; no code changes — cargo build/test not applicable.
- 2026-05-04T18:44:53.170835458+00:00 Task pending eval (agent reported done; awaiting `.evaluate-*` to score)
- 2026-05-04T18:46:33.678965603+00:00 PendingEval → Done (evaluator passed; downstream unblocks)