PROJ is CyberOS's project tracker, sprint engine, and Engagement billing surface in one. The data model is Issue -> Cycle -> Project plus Engagement (the contract). Status is a closed enum (backlog / todo / in-progress / in-review / done / cancelled) with a configurable per-project workflow on top. Priority is the standard urgent / high / medium / low / none. Mutations are optimistic locally, server-canonical eventually; Yjs CRDTs let two Members edit the same description offline and merge without conflict. Every issue can link to memory entries - a memory becomes a citation, a decision-log row becomes a sub-task. AI features are first-class: CUO drafts the cycle review at end-of-cycle; the blocker-detector watches comments for "blocked by"; estimate calibration tracks estimated vs actual hours per Member per task class.

The bigger picture - three strategic roles

PROJ is the orchestration spine of CyberOS - the module where every other module's data joins around a unit of work. Reading this page as "yet another Linear clone" misses the point: three distinct roles land in one Postgres schema, and the design treats them as equal-weight requirements.

Role 1 - orchestration spine

Every module's join lives here. CRM creates a deal -> PROJ converts it into an Engagement. TIME logs hours against an Issue, INV pulls those hours via the Engagement rate card. KB archives the cycle review. REW pulls Member calibration. OKR ties to Project status. Memory ingests every state change as a Layer-3 node. PROJ is where the threads of work cross - and where the audit trail of "did we do what we said we'd do" lands.

Role 2 - memory-anchored decisions

Issues cite memories, not stale prose. An Issue is a node that references a memory (the decision that spawned it, the spec it implements, the prior issue it supersedes). Every state change emits a chained audit row to the local memory. When the cycle closes, the review draft cites the actual decisions that drove the work - not "we decided to do X" without a source. This is the difference between a Jira ticket and a verifiable work record.

Role 3 - Engagement billing surface

Consultancies need this - product trackers assume it away. Linear assumes a product team. Jira assumes either. We assume consultancy delivery: a Client signs an Engagement at a billing mode (T&M / fixed-fee / retainer) with a localised rate card, time entries are billable-by-default with per-task overrides, and the TIME -> INV chain rides on the Engagement, not the Project. Pure product trackers cannot model this without bolting on a billing add-on; we model it natively.

The PROJ join surface (every module touches it)

flowchart LR
  CRM["CRM<br/>Deal won"]
  PROJ["PROJ<br/>Engagement / Project / Cycle / Issue"]
  TIME["TIME<br/>billable hours"]
  INV["INV<br/>billable line items"]
  KB["KB<br/>cycle review archive"]
  REW["REW<br/>calibration -> bonus"]
  OKR["OKR<br/>Project -> key result"]
  memory["memory<br/>Layer-3 ingest + audit chain"]
  PORTAL["PORTAL<br/>client-visible view"]
  EMAIL["EMAIL<br/>thread -> issue"]
  CRM -->|"deal.won -> engagement.create"| PROJ
  EMAIL -->|"client reply -> comment / issue"| PROJ
  PROJ -->|"issue tracked + rate card"| TIME
  TIME -->|"hours x rate (billable)"| INV
  PROJ -->|"cycle review, MD doc"| KB
  PROJ -->|"calibration snapshot"| REW
  PROJ -->|"project status"| OKR
  PROJ -->|"every mutation -> audit row"| memory
  PROJ -->|"client-visible Project view (P2)"| PORTAL
  memory -.->|"cite memory"| PROJ
  classDef hub fill:#e0e7ff,stroke:#3730a3,stroke-width:3px,color:#1e1b4b
  classDef other fill:#f5ede6,stroke:#45210e
  classDef memory fill:#fef6e0,stroke:#9c750a
  class PROJ hub
  class CRM,TIME,INV,KB,REW,OKR,PORTAL,EMAIL other
  class memory memory

Every arrow is a data dependency. Removing PROJ from the architecture means rebuilding the same join in 8 other places - that's why PROJ is the orchestration spine, not just "the issue tracker."

Auto vs manual operations matrix

OperationHow it happensWhy this split
Issue createManual - Member-driven in the SPAWork units must be intentional; auto-creating issues from chatter is noise.
Status transitionManual with WF validator + optimistic applyThe Member's act of moving an Issue is itself a meaningful event for the audit trail.
Blocker detectionAuto - pattern match on comments + dwell timeMembers forget to surface blockers; CUO Notify nudges the assigner without forcing a UI change.
Auto-roll at cycle closeAuto when project config enables itDefault off; teams opt in once they trust the roll behaviour. Saves an evening of dragging.
Cycle review draftAuto at cycle close, manual review by the AM before sendLLMs hallucinate; the AM is accountable for the prose; the draft saves 1-2 hours per cycle.
Memory Layer-3 ingestAuto on every state changeThe audit chain must be complete; opt-out is at the privacy class level (sync_class), not per-issue.
TIME -> INV roll-upAuto at invoice generationBillable hours computed from the Engagement rate card automatically; the AM reviews the draft invoice in INV.
Calibration snapshotAuto nightly batchThe data is too noisy to update per-issue; nightly aggregation is the cadence dashboards need.
Issue <-> memory linkManual in the SPA (with smart suggestions)The citation is a deliberate authoring act, not an LLM guess; we surface candidates but the Member confirms.

Why PROJ exists

Linear's data-model insight was that three primitives - Issue, Cycle, Project - cover almost every workflow that product and services teams actually do, and that a fourth primitive ("Engagement") covers the consultancy case once you add a rate card and a billable / non-billable distinction. The UX insight was the sync-engine: the user never waits for a server round-trip; the server is the eventual authority, but the local copy is the one being typed into. PROJ replicates both insights and adds the CyberOS-specific bindings: every issue links to memories (so a decision-log row can spawn a sub-task), every comment is a candidate for a blocker-detection nudge, every cycle ends with a CUO-drafted review the Account Manager can edit before sending to the Client.

The bet is that consultancies do not need a different tracker per client - they need one tracker that understands the consulting shape natively. Linear validated the three-primitive model; CyberOS extends it with the contract layer and the AI-native loop. The sync-engine is the rest of the way to "feels instant" - Linear documented the pattern; we re-implement it because the consulting cycle is two-weeks-anchored to a Client review, and that review needs to be CUO-draftable from the comment stream.

What it does - 5W1H2C5M

A structured decomposition of PROJ's scope.

AxisQuestionAnswer
5W - WhatWhat is PROJ?An issue tracker with four primitives (Issue, Cycle, Project, Engagement). Sync-engine for the SPA; Yjs CRDT for offline-edit merges; PGroonga for Vietnamese full-text; memory integration both ways (issues link to memories, completed issues ingested as Layer 3 nodes).
5W - WhoWho uses it?Members: create / assign / progress issues; review their own cycle queue daily. Account Managers: own the Engagement; edit + send CUO-drafted cycle reviews. Clients: read-only Project view via PORTAL (P2). Agents: CUO Notify on blocker detection; CUO/COO-skill triage suggestions.
5W - WhenWhen does it run?Continuous: WebSocket fan-out on every mutation; nightly batch for the estimate-calibration metric refresh; end-of-cycle batch for review draft generation. Auto-roll of incomplete tasks at cycle close (configurable per team).
5W - WhereWhere does it run?P1: single region (SG-1) with VN-residency S3 for tenant uploads. P3+: multi-region active-active. The sync server is a per-tenant axum binary; the SPA is hosted as static assets.
5W - WhyWhy a separate tracker?Existing trackers do not natively model the Engagement / rate-card / billable-time loop; sync-engine UX is rare outside Linear; and memory-linked work is a CyberOS-specific need that no off-the-shelf tracker provides.
1H - HowHow does it work?Mutations: client-side optimistic apply -> JMAP-like RPC over WebSocket -> server validates -> server-canonical state broadcast to all connected SPAs -> conflicts resolved server-side and rebased into the client. CRDT: long-form fields (description, comment body) are Yjs documents; the rest is last-write-wins with a vector clock.
2C - CostCost budget?P1: ~$80 / month for the SG-1 single-tenant pilot. 50-tenant: ~$340 / month (RDS + Redis + Fargate + S3). Per-issue write cost ~$0.0000004.
2C - ConstraintsConstraints?(a) Issues ingested into memory Layer 3 - but body content gates as per (task pending). (b) Per-task ACL - private engagements MUST NOT be visible to non-engaged Members. (c) Vietnamese localisation of status / UI strings - non-negotiable for P1 launch.
5M - MaterialsStack?Rust 1.81, axum 0.7, sqlx, PostgreSQL 16 + PGroonga, Redis 7, S3 + KMS, Yjs (server-side ywasm + client TS), TypeScript SPA (React + Zustand), WebSocket fan-out via NATS JetStream, OpenTelemetry SDK.
5M - MethodsMethod choices?Three-primitive model (Linear). Sync-engine UX (Linear). Yjs for CRDT long-form (because OT is too brittle at scale). Closed status enum + workflow overlay (not free-form labels). PGroonga for VN search. NATS JetStream for fan-out (not Postgres LISTEN/NOTIFY - won't scale past 1k WS connections).
5M - MachinesDeployment?Per-tenant Fargate axum binary handling WebSocket + REST. Postgres RDS Multi-AZ. Redis for hot-cache and WS rooms. NATS JetStream cluster (3 nodes P2+). S3 for attachments.
5M - ManpowerWho maintains?0.75 FTE (CTO seat) at P1 launch. By P2+: the COO seat owns product + Account Manager workflow; the CTO owns engine + sync layer.
5M - MeasurementHow measured?Issue write p95 <= 120 ms; SPA list-view load p95 <= 250 ms; offline-merge correctness >= 99.9% (property tests); memory ingestion p95 <= 5 s; cycle-review draft acceptance rate >= 60% by the Account Manager.

Orchestration spine - cross-module join contracts

PROJ is downstream of 2 modules (CRM, EMAIL) and upstream of 6 (TIME, INV, KB, REW, OKR, PORTAL); every other CyberOS module that touches "work" reads from or writes to a PROJ join contract. The contracts are stable interfaces - modules can be re-implemented behind them without breaking PROJ. The table below is the canonical list; every change to PROJ's data model must consider whether it widens a contract or breaks one.

CounterpartyDirectionJoin keyTriggerPayload shapeFailure mode
CRMCRM -> PROJcrm.deal_id -> engagement.source_deal_iddeal.stage = "won" + AM clicks "Convert to Engagement"{deal_id, client_account_id, value_vnd, contract_url, am_id}Manual fallback: the AM creates the Engagement directly; the deal link is added retroactively.
EMAILEMAIL -> PROJemail.thread_id -> issue.source_thread_id OR issue_id in commentMember uses "Convert to issue" or replies with a #PROJ-1234 mention{thread_id, from, subject, body, attachments[]}A thread reply with no PROJ tag stays in EMAIL; not auto-promoted.
TIMETIME -> PROJtime_entry.issue_id -> issue.idMember logs hours via TIME UI or timer auto-stop{issue_id, member_id, minutes, entry_date, billable_override?}If issue_id is missing or invalid the entry is tagged unallocated; the AM triages weekly.
INVPROJ -> INVengagement.id + cycle.id -> invoice.source_engagement_idINV draft invoice generation (monthly) or fixed-fee milestone{engagement_id, billable_hours_by_member_by_class, rate_card_snapshot, milestone_id?}If the rate card changed mid-cycle, INV uses the snapshot at time of entry - never retroactive.
KBPROJ -> KBcycle.review.id -> kb_doc.source_idAM clicks "Send + archive" on the cycle review{cycle_id, project_id, review_markdown_sent, sent_at, sent_to}If KB is unavailable, the review stays in PROJ; backfill via cyberos-kb backfill.
REWPROJ -> REWcalibration_snapshot.member_id -> rew.calibration_inputNightly batch (PROJ side) -> REW reads daily{member_id, period, ratio_by_task_class, completion_rate, blocker_authorship_rate}REW uses only Member-confirmed calibration signals; raw drift never feeds bonus directly.
OKRPROJ -> OKRproject.id -> okr_kr.project_linkOKR-side query: "what projects ladder to KR-Q3-rev?"{project_id, status, % issues done, cycle_review_links[]}The OKR view degrades to status-only if cycle reviews are absent.
PORTAL (P2)PROJ -> PORTALproject.id -> portal_view.project_id with client_visible=trueClient logs into PORTAL; PORTAL fetches via federated GraphQLSubset of project: title, status %, public cycles, public comments onlyRLS + client_visible flag enforced; misconfig = test failure in CI.
memoryBoth directionsissue.id <-> memory.path (MEMORY_LINK table)Mutation (auto-ingest); Member cites a memory (manual link)Outbound: audit row per mutation. Inbound: {path, relation: "cites" / "implements" / "supersedes"}Memory backlog > 60 s -> PROJ falls back to an in-memory queue + retry; alert on > 5 min.

Contract stability policy

Each join contract above is a versioned interface. Breaking changes require:

This is a deliberate brake: PROJ's data shape is the most expensive thing in CyberOS to mutate, because 8 downstream modules read from it.

Engagement economics - the consultancy-native primitive

The fourth primitive - Engagement - is where CyberOS earns its "consultancy-native" framing. A pure product tracker has Project but not the contract under which Project runs. A pure ERP has the contract but not the issues. PROJ owns both, joined by project.engagement_id. Below is the full economic model: billing modes, rate-card mechanics, and the per-Member, per-task-class billability rules that drive INV.

Three billing modes

ModeHow it worksWhat INV pullsRiskTypical use
T&M (Time & Materials)Each billable hour x rate-card row -> invoice line.SUM(time_entries WHERE billable=true) grouped by Member roleHours underestimated -> invoice surprise -> client disputes.Staff-aug deals, ongoing platform work, < 6 months
Fixed-feeTotal fee divided into milestones; INV invoices on milestone completion.Milestone marker (not time entries); time entries logged for cost analysis onlyScope creep eats the margin - see R-PROJ-013.Discrete deliverables, < 3 months, clear acceptance criteria
RetainerFlat monthly fee for capped hours; overage at T&M rate.Monthly invoice + overage hours if anyMember capacity assumed but not always available -> SLA risk.Ongoing advisory, maintenance, 6+ months

Rate card structure (per Engagement)

Every Engagement has 1..N rate-card rows. A row is (role x currency) -> hourly rate + billable default. Currency is fixed per row to avoid FX drift mid-cycle.

engagement: acme-q3-platform-build
billing_mode: T_AND_M
currency_primary: VND
rate_cards:
  - role: architect
    rate_vnd_per_hour: 2,500,000
    rate_usd_per_hour: 100
    billable_default: true
  - role: senior_engineer
    rate_vnd_per_hour: 1,800,000
    rate_usd_per_hour: 72
    billable_default: true
  - role: mid_engineer
    rate_vnd_per_hour: 1,200,000
    rate_usd_per_hour: 48
    billable_default: true
  - role: junior_engineer
    rate_vnd_per_hour: 700,000
    rate_usd_per_hour: 28
    billable_default: false  # juniors non-billable on this engagement
non_billable_categories:
  - "internal_meeting"
  - "training"
  - "rework_due_to_our_fault"

Billable / non-billable computation

The default cascade for "is this hour billable?":

  1. Member-level override on the TIME entry - if the Member set billable=false explicitly, that wins.
  2. Task class - if the Issue is labelled in non_billable_categories on the Engagement (e.g. internal_meeting, rework_due_to_our_fault), it is non-billable.
  3. Role default - fall back to rate_card.billable_default for the Member's role.
  4. Otherwise the hour is billable.

The decision is logged as a snapshot in the TIME entry (immutable) so retroactive rate-card changes don't shift past invoices.

Margin watchdog (planned, P2)

For fixed-fee engagements, the scope-creep risk (R-PROJ-013) needs an early warning. PROJ surfaces a per-Engagement margin chart: (fee x % milestone done) - (hours_burned x cost_per_hour). When projected margin drops below 30% of the original budget, CUO Notify pings the AM + CEO. This is a one-screen view in the SPA, not a separate module - because the consultancy founder needs to see it without context switching.

Memory-anchored decisions - issues cite memories

In a typical tracker, an Issue's why lives in stale prose ("Discussed in 5/14 standup - see Confluence page X" - which has since moved). In PROJ, an Issue's why lives in memory as a memory the Issue cites. When memory changes - a decision is superseded, a fact is corrected - the Issue's citation graph updates. This is the difference between a project history that ages well and one that decays.

Three citation relations

RelationMeaningExample
citesThis Issue is informed by, but doesn't fully implement, the memory."Issue: Add OTP rate-limit. Cites: memories/decisions/auth-flow-hardening.md."
implementsThis Issue is the implementation of a specific decision in the memory."Issue: Replace mocked DB with real Postgres. Implements: memories/decisions/proj-006-no-mocked-tests.md."
supersedesThis Issue replaces work that was previously done; the old work's memory is now historical."Issue: Migrate to new auth flow. Supersedes: memories/decisions/old-jwt-flow.md."

The decision -> sub-task pattern

A common workflow: a decision memory becomes a parent Issue with sub-tasks. The skill that handles this is cuo.cpo.decision-to-issues@1:

sequenceDiagram
  autonumber
  participant U as User ("convert this decision to issues")
  participant CU as CUO/CPO skill
  participant BR as memory read
  participant PR as PROJ
  participant BW as memory write (audit)
  U->>CU: invoke decision-to-issues@1 (memory path)
  CU->>BR: fetch memories/decisions/{slug}.md
  BR-->>CU: decision text + sub-decisions
  CU->>CU: extract action items via LLM (structured output)
  CU->>U: present draft (parent issue + N sub-tasks)
  U->>CU: confirm / edit
  CU->>PR: create_issue (parent) + create_issue x N (children, implements relation)
  PR->>BW: emit proj.issue_create x (N+1) audit rows, each citing the decision memory
  BW-->>PR: ok
  PR-->>U: issues created, memory-linked

This pattern is how a strategic decision in memory becomes verifiable work in PROJ - with a citation graph that survives leadership changes and tracker migrations.

Audit chain across PROJ and memory

Every PROJ mutation writes two audit rows: one in PROJ's own history_event table (fast, queryable) and one in the local memory (chained, signed, exportable). The memory row carries the same chain hash that PROJ's row references via history_event.memory_chain. This dual-write is the mechanism behind the "decisions live in memory, not Jira" claim - PROJ's local history is useful for the UI, but the cryptographic provenance lives in memory.

-- PROJ history_event row (Postgres, fast queries)
INSERT INTO history_event (id, issue_id, actor_id, kind, from_value, to_value, ts, memory_chain)
VALUES (
  'he_01HZ...',
  'is_01HZ...',
  'sub_stephen@...',
  'status',
  'in-progress',
  'in-review',
  '2026-05-15T08:42:11Z',
  '69e6...488a'   -- <- references the memory audit row's chain hash
);

-- memory audit row (immutable, chained, exportable)
{
  "seq": 18420,
  "ts_ns": 1763112131_000_000_000,
  "op": "put",
  "path": "memories/projects/proj-3110-status-transition.md",
  "actor": "sub_stephen@cyberskill.world",
  "extra": { "issue_code": "PROJ-3110", "from": "in-progress", "to": "in-review" },
  "prev_chain": "...",
  "chain": "69e6...488a"
}

Liquid-Glass UI surfaces - Board / Timeline / Gantt / Brief

PROJ is the design-system exemplar for the rest of CyberOS - every other module's table, timeline, and brief view borrows from PROJ's four canonical UI surfaces. The Liquid-Glass surface treatment (Part 21 of the design system: Umber #45210E base, Ochre #F4BA17 accent, blur backdrop, generous whitespace, Be Vietnam Pro 16px base) lands here first because PROJ is what Members look at all day.

SurfacePrimary useDefault viewDensityKeyboard-first?
Board (Kanban)Daily Member workflow - drag issues across columns.Current cycle, grouped by status, 5 columnsComfortable (rows ~56 px)Yes - j/k navigate, e edit, 1-5 status
TimelinePlan view - see what's happening this cycle by day.Cycle window, grouped by assignee, day columnsCompact (rows ~40 px)Yes - left/right arrows shift day, up/down arrows shift Member
GanttMulti-cycle planning - see Project-level dependencies.Project view, 3-month span, dependency arrowsCompact (bars ~28 px)Partial - +/- zoom, click-drag adjusts dates
BriefSingle-issue deep view - for editing / commenting / linking.Full-page modal with description (Yjs) + comments + meta sidebarSpacious (paragraph leading 1.6)Yes - c comment, l link memory, r review

Design tokens (PROJ-specific overlay)

PROJ inherits all tokens from website/docs/assets/tokens.css and adds an indigo overlay because the status colour palette is most readable in cool tones (warm anchors would clash with priority colours).

/* tokens.proj.css - PROJ-specific overlay (loaded after tokens.css) */
:root {
  --proj-indigo: #4f46e5;     /* primary action */
  --proj-indigo-bg: #eef2ff;  /* surface tint */
  --proj-indigo-dk: #3730a3;  /* on-hover, text accent */

  /* Status colours - readable on both light + dark surfaces */
  --status-backlog: #94a3b8;   /* slate-400 */
  --status-todo: #3b82f6;      /* blue-500 */
  --status-inprog: #f59e0b;    /* amber-500 */
  --status-inreview: #8b5cf6;  /* violet-500 */
  --status-done: #10b981;      /* emerald-500 */
  --status-cancelled: #64748b; /* slate-500, dimmed */

  /* Priority colours */
  --priority-urgent: #dc2626;  /* red-600 */
  --priority-high: #f97316;    /* orange-500 */
  --priority-medium: #eab308;  /* yellow-500 */
  --priority-low: #22c55e;     /* green-500 */
  --priority-none: transparent;

  /* Liquid-Glass surface (PROJ's primary card treatment) */
  --proj-glass-bg: rgba(248, 250, 252, 0.72);
  --proj-glass-border: rgba(79, 70, 229, 0.12);
  --proj-glass-blur: blur(18px) saturate(180%);
}

Accessibility commitments

Architecture

PROJ is one axum binary with four surfaces (sync-engine WebSocket, REST admin, GraphQL federated subgraph, MCP tool catalogue) and three stores (Postgres for canonical state + Yjs document snapshots, Redis for WS rooms and search cache, S3 for attachments). NATS JetStream fans mutations out to every connected SPA.

graph TB
  subgraph CLIENT ["Clients"]
    SPA["SPA<br/>React + Zustand"]
    MOBILE["Mobile (P3)"]
    AGENT["CUO agent<br/>via MCP"]
  end
  subgraph EDGE ["Edge"]
    WSGW["WebSocket gateway<br/>JWT-authed"]
    GQL["GraphQL subgraph<br/>(federated)"]
    REST["Admin REST"]
    MCP["MCP tool surface"]
  end
  subgraph CORE ["PROJ service (Rust)"]
    SYNC["Sync engine<br/>optimistic + rebase"]
    CRDT["Yjs document server<br/>(ywasm)"]
    WF["Workflow engine<br/>status transitions"]
    AI["AI hooks<br/>blocker / review / calibration"]
    MEMORY_ING["memory ingestor<br/>Layer 3"]
    AUTH_CHK["RBAC + per-task ACL"]
  end
  subgraph STORES ["Stores"]
    PG[("PostgreSQL + PGroonga<br/>issues / cycles / projects<br/>RLS by tenant_id")]
    RED[("Redis 7<br/>WS rooms / search cache")]
    S3[("S3 + KMS<br/>attachments")]
    NATS[("NATS JetStream<br/>mutation fan-out")]
  end
  subgraph SINKS ["Sinks"]
    memory["memory<br/>Layer 3 ingest / audit"]
    TIME["TIME<br/>billable / non-billable"]
    INV["INV<br/>billable hours -> invoice"]
    OBS["OBS<br/>traces + SLO"]
    CUO["CUO<br/>Notify / review draft"]
  end
  SPA --> WSGW
  MOBILE --> WSGW
  AGENT --> MCP
  WSGW --> SYNC
  GQL --> SYNC
  REST --> SYNC
  MCP --> SYNC
  SYNC --> CRDT
  SYNC --> WF
  SYNC --> AI
  SYNC --> MEMORY_ING
  SYNC --> AUTH_CHK
  SYNC --> PG
  SYNC --> RED
  SYNC --> NATS
  NATS --> WSGW
  CRDT --> PG
  AI --> CUO
  MEMORY_ING --> memory
  WF --> TIME
  TIME --> INV
  SYNC --> S3
  SYNC --> OBS
  classDef planned fill:#cba88a,stroke:#45210e
  classDef store fill:#f5f3ff,stroke:#7c3aed
  classDef sink fill:#f5ede6,stroke:#45210e
  class SPA,MOBILE,AGENT,WSGW,GQL,REST,MCP,SYNC,CRDT,WF,AI,MEMORY_ING,AUTH_CHK planned
  class PG,RED,S3,NATS store
  class memory,TIME,INV,OBS,CUO sink

The four primitives

Internal components

The cyberos-proj crate is building TASK-PROJ-001 slice 1 - Issue / Cycle / Engagement with RLS, the status FSM, and memory-audit emission. The table lists the module's planned file layout; rows marked (task pending) are not yet built.

ComponentPath (planned)Responsibility
sync.rsservices/proj/src/sync.rsSync-engine - optimistic apply, conflict detection, server-canonical broadcast.
crdt.rsservices/proj/src/crdt.rsYjs document server via ywasm. Long-form fields (description, comment body) are Yjs; the rest is LWW with a vector clock.
status_fsm.rsservices/proj/src/status_fsm.rsClosed status enum plus the per-project workflow overlay; validates every transition.
blockers.rsservices/proj/src/blockers.rsBlocker detection from the comment stream. "blocked by", "waiting on", and dwell-time heuristics -> CUO Notify.
cycle_review.rsservices/proj/src/cycle_review.rsEnd-of-cycle review draft generator. Pulls from issue changelog + comments; CUO-stamped persona; the AM edits before sending.
estimate.rsservices/proj/src/estimate.rsEstimate vs actual hours per Member per task class. Surfaces calibration drift to the Member's dashboard.
memory_ingest.rsservices/proj/src/memory_ingest.rsLayer 3 ingestion of issues + comments (task pending). Body content gates per ACL.
acl.rsservices/proj/src/acl.rsPer-task / per-engagement ACL. Private engagements not visible to non-engaged Members (task pending).
autoroll.rsservices/proj/src/autoroll.rsEnd-of-cycle auto-roll. Incomplete issues move to the next cycle if team config permits (task pending).
nats_fanout.rsservices/proj/src/nats_fanout.rsNATS JetStream publisher / subscriber for mutation fan-out.
search.rsservices/proj/src/search.rsPGroonga query builder; VN bigram tokenisation.
graphql.rsservices/proj/src/graphql.rsFederated subgraph; @key(fields:"id") on every primitive.
mcp.rsservices/proj/src/mcp.rsMCP tool catalogue exported to CUO.
i18n.rsservices/proj/src/i18n.rsvi + en string tables; locale resolved from the JWT claim; status names localised.
migrations/services/proj/migrations/sqlx migrations with RLS on every table.

Data model

The canonical state is PostgreSQL with row-level security by tenant_id and, for private engagements, additional ACL enforcement by engagement_id membership. Long-form fields are Yjs document snapshots stored as bytea blobs; the active CRDT state lives in Redis for the duration of a session.

erDiagram
  TENANT ||--o{ ENGAGEMENT: "has"
  ENGAGEMENT ||--o{ PROJECT: "delivered as"
  ENGAGEMENT ||--o{ RATE_CARD: "carries"
  PROJECT ||--o{ ISSUE: "contains"
  PROJECT ||--o{ CYCLE: "has"
  CYCLE ||--o{ ISSUE: "schedules"
  ISSUE ||--o{ COMMENT: "has"
  ISSUE ||--o{ DEPENDENCY: "depends on"
  ISSUE ||--o{ LABEL_ASSIGNMENT: "tagged"
  ISSUE ||--o| ASSIGNEE: "assigned to"
  ISSUE ||--o{ ATTACHMENT: "has"
  ISSUE ||--o{ TIME_ENTRY: "logged against"
  PROJECT ||--o{ WORKFLOW: "uses"
  WORKFLOW ||--o{ WORKFLOW_STATE: "defines"
  ISSUE ||--o{ HISTORY_EVENT: "audited"
  ISSUE ||--o{ MEMORY_LINK: "cites"
  CYCLE ||--o| CYCLE_REVIEW: "summarised"
  TENANT {
    uuid id PK
    string slug
  }
  ENGAGEMENT {
    uuid id PK
    uuid tenant_id FK
    uuid client_account_id FK "links to CRM"
    string name
    date start_date
    date end_date
    string acl_mode "open | restricted"
    string billing_mode "T&M | fixed-fee | retainer"
  }
  RATE_CARD {
    uuid id PK
    uuid engagement_id FK
    string role "senior | mid | junior | architect"
    int rate_vnd_per_hour
    int rate_usd_per_hour
    bool billable_default
  }
  PROJECT {
    uuid id PK
    uuid engagement_id FK
    string name
    string status "planned | active | paused | completed"
    bool client_visible
    timestamp created_at
  }
  CYCLE {
    uuid id PK
    uuid project_id FK
    int number
    date start_date
    date end_date
    int velocity_target
    bool autoroll_enabled
  }
  ISSUE {
    uuid id PK
    uuid project_id FK
    uuid cycle_id FK "nullable (backlog)"
    string code "PROJ-1234"
    string title
    bytea description_yjs "Yjs doc snapshot"
    string status "backlog|todo|in-progress|in-review|done|cancelled"
    string priority "urgent|high|medium|low|none"
    uuid assignee_id FK
    int estimate_hours
    int actual_hours
    uuid parent_issue_id FK "nullable"
    timestamp created_at
    timestamp updated_at
    uuid created_by FK
  }
  COMMENT {
    uuid id PK
    uuid issue_id FK
    uuid author_id FK
    bytea body_yjs "Yjs doc"
    timestamp created_at
  }
  DEPENDENCY {
    uuid from_issue_id FK
    uuid to_issue_id FK
    string kind "blocks | relates_to | duplicates"
  }
  LABEL_ASSIGNMENT {
    uuid issue_id FK
    string label
  }
  ATTACHMENT {
    uuid id PK
    uuid issue_id FK
    string filename
    string s3_key
    bigint size_bytes
  }
  TIME_ENTRY {
    uuid id PK
    uuid issue_id FK
    uuid member_id FK
    int minutes
    date entry_date
    bool billable
    string source "TIME"
  }
  WORKFLOW {
    uuid id PK
    uuid project_id FK
    string name
  }
  WORKFLOW_STATE {
    uuid id PK
    uuid workflow_id FK
    string code
    string display_name_vi
    string display_name_en
    int order_idx
    string category "todo | in-progress | done | cancelled"
  }
  HISTORY_EVENT {
    uuid id PK
    uuid issue_id FK
    uuid actor_id FK
    string kind "create | status | assign | comment | ..."
    string from_value
    string to_value
    timestamp ts
    string memory_chain
  }
  MEMORY_LINK {
    uuid issue_id FK
    string memory_path "memories/decisions/..."
    string relation "cites | implements | supersedes"
  }
  CYCLE_REVIEW {
    uuid cycle_id PK
    string draft_markdown
    string sent_markdown
    timestamp generated_at
    timestamp sent_at
  }

Status enum + workflow overlay

The status enum is closed (backlog / todo / in-progress / in-review / done / cancelled). Per-project workflows can rename and re-order the visible states but always map back to a category in the closed set. The category is what drives velocity calculation, auto-roll, and memory ingestion.

Statusvi nameen nameCategoryVelocity counts?
backlogTồn đọngBacklogtodono
todoCần làmTodotodono
in-progressĐang làmIn progressin-progressyes
in-reviewĐang reviewIn reviewin-progressyes
doneHoàn thànhDonedoneyes
cancelledHuỷ bỏCancelledcancelledno

API surface

Four surfaces: a WebSocket sync-engine for the SPA, a GraphQL federated subgraph for cross-module queries, an admin REST for migration and bulk import, and an MCP tool catalogue for CUO. All four share the same RBAC predicate.

GraphQL subgraph (federated)

extend schema
 @link(url: "https://specs.apollo.dev/federation/v2.5", import: ["@key", "@requiresScopes"])

type Engagement @key(fields: "id") {
 id: ID!
 name: String!
 clientAccountId: ID!
 startDate: Date!
 endDate: Date
 billingMode: BillingMode!
 projects: [Project!]!
 rateCards: [RateCard!]!
}

type Project @key(fields: "id") {
 id: ID!
 engagementId: ID!
 name: String!
 status: ProjectStatus!
 clientVisible: Boolean!
 cycles(active: Boolean): [Cycle!]!
 issues(filter: IssueFilter, limit: Int = 50, cursor: String): IssueConnection!
}

type Cycle @key(fields: "id") {
 id: ID!
 projectId: ID!
 number: Int!
 startDate: Date!
 endDate: Date!
 velocityTarget: Int
 issues: [Issue!]!
 review: CycleReview
}

type Issue @key(fields: "id") {
 id: ID!
 code: String!
 title: String!
 description: String! # rendered from Yjs doc
 status: IssueStatus!
 priority: IssuePriority!
 assignee: Subject
 estimateHours: Int
 actualHours: Int
 cycleId: ID
 parent: Issue
 children: [Issue!]!
 dependencies: [Dependency!]!
 comments: [Comment!]!
 labels: [String!]!
 attachments: [Attachment!]!
 memoryLinks: [MemoryLink!]!
 history: [HistoryEvent!]!
}

enum IssueStatus { BACKLOG TODO IN_PROGRESS IN_REVIEW DONE CANCELLED }
enum IssuePriority { URGENT HIGH MEDIUM LOW NONE }
enum ProjectStatus { PLANNED ACTIVE PAUSED COMPLETED }
enum BillingMode { T_AND_M FIXED_FEE RETAINER }

type Mutation {
 createIssue(input: CreateIssueInput!): Issue!
 @requiresScopes(scopes: [["proj.write"]])
 updateIssue(id: ID!, patch: IssuePatch!): Issue!
 assignIssue(id: ID!, assigneeId: ID!): Issue!
 moveIssueToCycle(id: ID!, cycleId: ID): Issue!
 addComment(issueId: ID!, body: String!): Comment!
 closeCycle(cycleId: ID!): CycleReview!
 @requiresScopes(scopes: [["proj.cycle.close"]])
}

Sync-engine WebSocket protocol

// Client -> server: optimistic mutation
{
 "op": "issue.update",
 "client_seq": 12834,
 "tenant_id": "01HZ...",
 "issue_id": "01HZ...",
 "patch": { "status": "in-progress", "assignee_id": "01HZ..." },
 "vector_clock": { "spa-stephen-laptop": 47 }
}

// Server -> all room subscribers: canonical state
{
 "op": "issue.state",
 "server_seq": 982341,
 "issue_id": "01HZ...",
 "state": { "status": "in-progress", "assignee_id": "01HZ...", "updated_at": "..." },
 "vector_clock": { "spa-stephen-laptop": 47, "server": 982341 }
}

// Server -> client (conflict, rebase needed)
{
 "op": "rebase",
 "client_seq": 12834,
 "server_state": { ... },
 "reason": "concurrent_modification"
}

MCP tool catalogue

Tool nameInputsOutputsAnnotations
cyberos.proj.list_issuesproject_id?, cycle_id?, assignee_id?, status?Issuereadonly, scope=proj.read
cyberos.proj.create_issueCreateIssueInputIssuescope=proj.write
cyberos.proj.update_issueid, patchIssuescope=proj.write
cyberos.proj.assign_issueid, assignee_idIssuescope=proj.write
cyberos.proj.add_commentissue_id, bodyCommentscope=proj.write
cyberos.proj.move_to_cycleid, cycle_idIssuescope=proj.write
cyberos.proj.close_cyclecycle_idCycleReview (draft)destructive, human-confirm, scope=proj.cycle.close
cyberos.proj.detect_blockersproject_id?Issue + reasonsreadonly
cyberos.proj.estimate_calibrationmember_id, rangeCalibration statsreadonly

Key flows

Flow 1 - create an issue (sync-engine)

sequenceDiagram
  autonumber
  participant U as Member SPA
  participant WS as WebSocket gateway
  participant S as Sync engine
  participant W as Workflow validator
  participant ACL as ACL check
  participant PG as PostgreSQL
  participant N as NATS JetStream
  participant B as memory audit
  participant OTHER as Other connected SPAs
  U->>U: optimistic local insert (status=todo)
  U->>WS: op:issue.create (client_seq:N)
  WS->>S: dispatch
  S->>ACL: check (subject, action:create, project_id)
  ACL-->>S: allow
  S->>W: validate status transition
  W-->>S: ok
  S->>PG: INSERT issue + history_event
  S->>B: proj.issue_create (chained audit row)
  S->>N: publish proj.issue.{project_id}
  N->>WS: fan-out
  WS->>OTHER: op:issue.state (server_seq:M)
  WS-->>U: op:issue.state (canonical)
  Note over U: local optimistic state reconciled with server-canonical

Flow 2 - concurrent edit + conflict-free merge via Yjs

sequenceDiagram
  autonumber
  participant A as Member A (offline)
  participant B as Member B (online)
  participant Y as Yjs document server
  participant PG as PostgreSQL
  participant N as NATS
  A->>A: edit description offline (vector clock advances)
  B->>Y: op:issue.description.update (online)
  Y->>PG: snapshot description_yjs
  Y->>N: publish issue.description.{id}
  Note over A: A comes back online
  A->>Y: sync (vector clock diff)
  Y->>Y: merge A's edits + B's edits (CRDT)
  Y->>PG: snapshot merged Yjs doc
  Y->>N: publish merged state
  Y-->>A: rebased state
  Y-->>B: rebased state
  Note over Y: no conflict resolution prompt - CRDT semantics handle it

Yjs handles the description / comment-body fields. Scalar fields (status, priority, assignee) use last-write-wins with the server's vector clock as the tiebreaker.

Flow 3 - AI cycle-review draft at cycle close

sequenceDiagram
  autonumber
  participant CL as Cron / cycle close trigger
  participant S as Sync engine
  participant A as AI review generator
  participant AG as AI Gateway
  participant CU as CUO persona
  participant PG as PostgreSQL
  participant N as Notify (CHAT) to AM
  participant AM as Account Manager
  participant B as memory audit
  CL->>S: cycle.end_date reached
  S->>A: assemble inputs (issues, comments, time entries, velocity)
  A->>AG: chat.completions (persona=CUO/COO-skill, cycle review template)
  AG-->>A: draft markdown
  A->>PG: cycle_review.draft_markdown = draft
  A->>N: ping AM "draft ready"
  A->>B: proj.cycle_review_draft
  AM->>S: edit draft
  AM->>S: send to client (via EMAIL)
  S->>B: proj.cycle_review_sent

Flow 4 - blocker detection from comments

sequenceDiagram
  autonumber
  participant U as Commenter
  participant S as Sync engine
  participant BD as Blocker detector
  participant CU as CUO
  participant N as Notify
  participant B as memory audit
  U->>S: add_comment "blocked by waiting on client decision"
  S->>BD: classify comment
  BD->>BD: pattern match ("blocked by", "waiting on", dwell time > 48h)
  BD-->>S: flag {kind:"external_blocker", confidence:0.91}
  S->>CU: enqueue notify to assigner
  CU->>N: ping assigner with issue link
  S->>B: proj.blocker_detected

Flow 5 - estimate calibration nightly batch

sequenceDiagram
  autonumber
  participant CR as Nightly cron
  participant CAL as Calibration engine
  participant PG as PostgreSQL
  participant BR as memory write
  participant DASH as Member dashboard
  CR->>CAL: run for tenant
  CAL->>PG: SELECT estimate, actual hours per member per task_class
  PG-->>CAL: rows
  CAL->>CAL: compute ratio, drift, 7-day moving avg
  CAL->>BR: write calibration summary (memory Layer 3)
  CAL->>PG: write calibration_snapshot rows
  DASH->>PG: query and render the Member's calibration chart

Issue lifecycle

An issue traverses six canonical states (the closed enum). The per-project workflow overlay may rename or re-order the visible states but the underlying category drives velocity, auto-roll, and memory ingestion.

stateDiagram-v2
  [*] --> Backlog: created (no cycle)
  Backlog --> Todo: scheduled into cycle
  Todo --> InProgress: assignee starts
  InProgress --> InReview: PR opened / requesting review
  InReview --> InProgress: reviewer requested changes
  InReview --> Done: approved + merged
  Todo --> Cancelled: discarded
  InProgress --> Cancelled: discarded
  Done --> [*]
  Cancelled --> [*]
  Backlog --> Cancelled: discarded from backlog

Auto-roll behaviour at cycle close

Issue status at cycle closeAuto-roll behaviourConfigurable?
todoRoll to next cycleyes (task pending)
in-progressRoll to next cycleyes
in-reviewRoll to next cycleyes
doneStay in original cycle (velocity counts)no
cancelledStay in original cycleno
backlog (unscheduled)Stay in backlogn/a

Functional requirements

The CyberOS task catalogue is being rebuilt one feature at a time via the open task-author Agent Skill.

Previous task enumerations were archived 2026-05-14 and are no longer reflected on this page. Specific tasks land here as they are re-authored.

Non-functional requirements

NFRs that PROJ must satisfy. Cross-referenced at nfr-catalog.html#proj.

NFR IDConcernTargetMeasurement
N(task pending)Issue update mutationp95 <= 120 ms server-sidehistogram via OBS
N(task pending)Issue list view (50 issues)p95 <= 250 ms (network + render)SPA RUM
N(task pending)WebSocket fan-out latency (mutation -> other clients)p95 <= 200 ms within regionNATS lag metric
N(task pending)Memory ingestion (issue create -> indexed)p95 <= 5 smemory ingest histogram
N(task pending)Offline-edit merge correctness>= 99.9%property-based test (10k random workloads)
N(task pending)Vietnamese search recall (50-query corpus)>= 90%quarterly eval
N(task pending)Cycle-review draft acceptance rate>= 60% by AMSPA telemetry
N(task pending)Service availability (28-day)>= 99.9%OBS SLO monitor
N(task pending)Private-engagement isolation= 0 cross-leakCI test on every PR
N(task pending)RLS coverage100% of tablesmigration-CI gate
N(task pending)Mutation durability after WS ack= 0 dropped under crashchaos test + memory walk
N(task pending)WS connections per tenant per axum task>= 5k sustainedk6 load test

Dependencies

PROJ depends on AUTH for every call, memory for ingestion + audit, TIME for time entries, AI for CUO features, MCP for tool surfacing, and NATS for fan-out. It is depended on by INV (billable hours roll-up), CRM (deal -> engagement link), KB (project docs), and PORTAL (client-visible Project view at P2+).

graph LR
  subgraph upstream ["PROJ depends on"]
    AUTH["AUTH"]
    memory["memory"]
    TIME["TIME"]
    AI["AI Gateway"]
    MCP["MCP"]
    NATS["NATS JetStream"]
    OBS["OBS"]
  end
  PROJ["PROJ"]
  subgraph downstream ["PROJ is depended on by"]
    INV["INV<br/>billable roll-up"]
    CRM["CRM<br/>deal -> engagement"]
    KB["KB<br/>project docs"]
    EMAIL["EMAIL<br/>thread -> issue"]
    PORTAL["Portal (P2)"]
  end
  AUTH --> PROJ
  memory --> PROJ
  TIME --> PROJ
  AI --> PROJ
  MCP --> PROJ
  NATS --> PROJ
  OBS --> PROJ
  PROJ --> INV
  PROJ --> CRM
  PROJ --> KB
  EMAIL --> PROJ
  PROJ --> PORTAL
  classDef shipped fill:#f5ede6,stroke:#45210e
  classDef planned fill:#fef6e0,stroke:#9c750a
  class memory shipped
  class PROJ,AUTH,TIME,AI,MCP,NATS,OBS,INV,CRM,KB,EMAIL,PORTAL planned

Compliance scope

PROJ is not a regulator's first stop, but it sits inside the audit trail for delivery work and must satisfy ACL, audit, and DSAR obligations.

Regulation / standardArticle / clausePROJ feature that satisfies it
Vietnam PDPL (Law 91/2025)Art. 14 - DSARDSAR export includes every issue / comment a subject authored.
Vietnam Decree 13/2023Art. 17 - Processing logThe HISTORY_EVENT table is the processing log; every mutation writes one row.
GDPR (EU 2016/679)Art. 17 - Right to erasureSoft-tombstone on assignee, then DSAR-driven hard purge via memory.
GDPRArt. 25 - Privacy by designPrivate-engagement ACL means non-engaged Members cannot see issue titles.
ISO/IEC 27001:2022A.8.2 - Privileged accessRBAC + per-engagement ACL gate write operations.
ISO/IEC 27001:2022A.8.16 - MonitoringThe memory audit chain covers create / status / assign / comment events.
SOC 2 Type IICC6.1 - Logical accessRLS + ACL enforcement at every mutation.
SOC 2 Type IICC7.2 - System monitoringOBS traces + memory audit cross-correlate.

Risk entries

PROJ-specific risks tracked in the risk register.

IDRiskLikelihoodImpactOwnerMitigation
R-PROJ-001Sync-engine state divergence between SPA and serverMediumHighCTOProperty-based tests (10k random workloads); server is canonical; client always rebases on conflict.
R-PROJ-002Yjs CRDT memory leak on long-running document sessionsMediumMediumCTOSnapshot to Postgres every 60 s; evict in-memory doc after 5 min idle; reload on demand.
R-PROJ-003Private-engagement leak via cross-tenant graph queryLowHighCSORLS + ACL CI gates; quarterly red-team review of the GraphQL surface.
R-PROJ-004NATS JetStream backlog -> WS clients see stale stateMediumMediumCTONATS depth alerted at > 1k msgs; SPA reconnect triggers a full state refetch.
R-PROJ-005Auto-roll inflates the next cycle and burns out the teamMediumMediumCOOAuto-roll opt-in per project; the AM sees the roll-over count in the cycle review draft.
R-PROJ-006Cycle-review draft hallucinates events that did not occurMediumMediumCOOThe draft is an editable suggestion, never auto-sent; the AM is accountable for content.
R-PROJ-007Blocker detector false-positive spamMediumLowCOOConfidence threshold >= 0.85; user "not a blocker" feedback trains down.
R-PROJ-008Memory ingestion lag breaks cross-module searchLowMediumCDOp95 <= 5 s SLO; backlog alarm pages CDO + CTO.
R-PROJ-009Calibration data weaponised in performance reviewMediumLowCHROCalibration visible only to Member + manager + CHRO; never client-visible; never used as a sole performance signal (HR policy).
R-PROJ-010Vietnamese tokenisation regressions on PGroonga upgradeLowLowCTO50-query VN test corpus run on every PGroonga upgrade.
R-PROJ-011Orchestration spine becomes a single point of failure - a PROJ outage cascades to TIME / INV / KB / REW / OKR / PORTAL simultaneouslyLowCriticalCTOSLO >= 99.9% with multi-AZ Postgres + Redis + NATS; degraded-mode reads from the memory audit chain when the PROJ DB is unreachable; OBS-driven dependency map surfaces the fallout radius; quarterly chaos test cuts PROJ off and verifies downstream module graceful degradation.
R-PROJ-012Join-contract breaking change ships without ADR + counterparty co-signMediumHighCTOCI gate on services/proj/contracts/* requires > 1 module owner approval; contract schema versioned + frozen at minor releases; deprecation window enforced via a dual-shape test.
R-PROJ-013Fixed-fee scope creep eats margin - work delivered exceeds milestone scope, margin drops below 30% before the AM noticesHighHighCOOMargin watchdog at P2: per-Engagement margin chart + CUO Notify when projected margin < 30%; mandatory weekly AM review of fixed-fee deals; a "scope change order" workflow surfaces a billable change.
R-PROJ-014Memory citation drift - an Issue cites a memory path that's been moved or tombstonedMediumLowCDOThe MEMORY_LINK table stores the chain hash at time of citation; on memory move/tombstone, the citation is flagged stale + the Member sees a UI nudge to update; never auto-broken because memory is append-only at the chain layer.
R-PROJ-015AI cycle-review draft cites work that was actually done in another cycleMediumMediumCOOThe draft generator is constrained to events with ts inside the cycle window; the LLM prompt includes explicit boundary dates; the AM signs off as the human-in-loop; never auto-sent.
R-PROJ-016Engagement billing-mode change mid-cycle creates an inconsistent invoiceLowHighCOOA billing-mode change requires an explicit "effective_from" date + AM sign-off + INV draft preview; time entries before that date use the prior mode's rate-card snapshot (immutable).
R-PROJ-017The decision-to-issues skill produces a parent + children that drift from the memory's intentMediumMediumCPOThe skill outputs a confidence score per sub-task; the Member must confirm before issues are created; a CI replay test ensures > 80% of historical decision memories produce semantically aligned issues (LLM-judged on a fixed eval set).
R-PROJ-018The Liquid-Glass surface fails accessibility at lower contrast on Members' laptopsMediumMediumCDOaxe-core CI gate on the Storybook build; per-pill contrast measured against the worst-case backdrop (busy timeline view); a failing component blocks the PR.
R-PROJ-019SPA cold-load p95 > 5 s on Vietnamese mobile networks - Members give up and use ExcelMediumHighCTOSPA bundle < 220 KB gzipped; route-level code-split; preload the critical font subset (VN diacritics) inline; RUM monitor in OBS targeting Viettel/Mobifone networks specifically.
R-PROJ-020NATS JetStream backlog after a multi-AZ failover leaves SPAs displaying stale state for > 30 sLowMediumCTONATS depth gauge alerted at > 1k msgs; SPA reconnect after WS close triggers a full state refetch; a degraded-mode banner is shown to the Member.

KPIs

PROJ rolls up KPIs covering delivery cadence, sync-engine performance, AI feature efficacy, and Member experience.

KPIFormulaSourceTarget
Cycle velocity stabilitystddev(completed_points) / meancycle_review<= 0.25 (steady)
Issue update mutation p95histogramOBS<= 120 ms
WS fan-out lag p95histogramNATS<= 200 ms
Auto-rolled issues per cyclecountcycle_reviewtracked; alert if > 30%
AI cycle-review acceptancesent / draftedSPA telemetry>= 60%
Blocker-detection precisiontrue_positive / flaggeduser feedback>= 80%
Memory ingestion lag p95histogrammemory<= 5 s
Estimate calibration driftactual / estimate - 1calibration_snapshottracked per Member
Offline-merge incidentsconflicts / monthmemory audittracked; expect < 5 / mo
Join-contract stabilitybreaking changes / quarter, weighted by counterparty countcontract-schema CI gate<= 1 per quarter (with full ADR + dual-shape window)
Engagement margin (T&M)(revenue - cost) / revenue per EngagementINV + cost ledgertracked per Engagement; target >= 35%
Engagement margin (fixed-fee)(fee - hours_burned x cost_per_hour) / feePROJ time entries + INV>= 30% on close; alert if projection < 30% mid-engagement (R-PROJ-013)
Issues with memory citationissues_with_memory_link / total issuesMEMORY_LINK tabletracked; expect >= 40% of high-priority issues to cite a memory
Decision-to-issues skill acceptancedrafts confirmed by Member / drafts generatedcuo.cpo.decision-to-issues@1 telemetry>= 70%
SPA cold-load p95 (VN mobile)RUM Time-to-Interactive on Viettel/Mobifone networksOBS RUM<= 3.5 s P50, <= 5 s P95
Citation-drift rateMEMORY_LINKs flagged stale / total MEMORY_LINKs, 28-day windowMEMORY_LINK + memory audit<= 5%
Cross-tenant ACL rejection rateRLS / ACL rejections / total queriesPostgres audit logtracked; spike = security alert
Dogfooding cycle-review draft acceptance (internal)internal team draft accept-rateSPA telemetry filtered to tenant_id=org:cyberskill>= 70% (the founders use this themselves before selling it)

RACI matrix

PROJ is owned by the COO seat. Today (COO vacant), the CEO is interim accountable; the CTO owns engineering; Account Managers own per-engagement workflow configuration.

ActivityCEOCTOCOOCHROCDOAM
Service design + specARCICC
Sync-engine implementationIA/RIIII
Engagement / workflow configICAIIR
Cycle review sendIICIIA/R
Calibration policy (HR)CICA/RII
Memory ingestion reviewICIIA/RI
Private-engagement ACL auditCRAIII
VN localisation reviewCCA/RIIC
Incident responseARRICI

R Responsible, A Accountable, C Consulted, I Informed.

Planned CLI surface

A single admin CLI cyberos-proj for tenant operators and an SDK for scripted bulk edits.

1. Create an Engagement

$ cyberos-proj engagement create \
 --tenant cyberskill \
 --client-account acme-vn \
 --name "ACME Q3 platform build" \
 --billing-mode T_AND_M \
 --start 2026-07-01

[engagement created]
 id: 01HZK2...
 client: acme-vn
 billing: T_AND_M
 audit: memory seq=15014 chain=1f2e...3d4c

2. Create a Cycle and bulk-create Issues

$ cyberos-proj cycle create --project pj-acme-platform --number 7 --start 2026-08-04 --weeks 2
[cycle created] id=01HZK3... number=7 dates=2026-08-04 / 2026-08-17

$ cyberos-proj issue bulk-create --cycle 01HZK3... --file backlog.yaml
[bulk-create] parsed 12 issues
[bulk-create] PROJ-1247 "Auth integration" -> assignee=linh@...
[bulk-create] PROJ-1248 "Migration runbook" -> assignee=tu@...
[bulk-create] ... 10 more
[bulk-create] 12 created, 0 errors
[audit] memory seq=15018 chain=...

3. Close a Cycle (auto-generate review draft)

$ cyberos-proj cycle close --id 01HZK3...

[cycle close] 12 issues - 8 done, 3 rolled, 1 cancelled
[review draft] generated via CUO/COO-skill
[review draft] saved to ./cycle-7-review.md (124 lines)
[notify] Account Manager pinged in CHAT
[audit] memory seq=15042 chain=...

4. Move all in-progress issues to the next cycle

$ cyberos-proj cycle rollover --from 01HZK3... --to 01HZK4... --status in-progress

[rollover] 3 issues moved
[rollover] PROJ-1249, PROJ-1251, PROJ-1255
[audit] memory seq=15045 chain=...

5. Estimate calibration report

$ cyberos-proj calibration --member linh@cyberskill.world --range 90d

member: linh@cyberskill.world
task_class estimates actual_avg ratio trend
backend.feature 18 1.18x +18% improving
infra.config 9 0.96x -4% stable
qa.test_plan 12 1.42x +42% widening
overall 39 1.18x +18% improving

6. Export a DSAR bundle

$ cyberos-proj dsar-export --subject linh@cyberskill.world --output linh-proj.zip

[dsar] subject: linh@cyberskill.world
[dsar] issues: 218 (authored), 612 (assigned)
[dsar] comments: 1,894
[dsar] history: 4,217
[dsar] written: linh-proj.zip (38 MB)

7. Replay a sync-engine conflict for debug

$ cyberos-proj sync replay --tenant cyberskill --since 1h --filter "issue.update"

[replay] 47 mutations, 2 rebases, 0 lost
[rebase] PROJ-1247 client_seq=18 server_seq=98234 reason=concurrent_modification
[rebase] PROJ-1255 client_seq=22 server_seq=98241 reason=concurrent_modification

Phase status & estimates

CapabilityStatus
Four primitives (Issue / Cycle / Project / Engagement)planned, P1
Sync-engine WS + optimistic applyplanned, P1
Yjs CRDT for description / commentsplanned, P1
Per-engagement rate cardsplanned, P1
TIME <-> PROJ <-> INV chainplanned, P1
Auto-roll at cycle closeplanned, P1
AI blocker detectionplanned, P1
AI cycle-review draftplanned, P1
Estimate calibration reportplanned, P1
Memory Layer 3 ingestionplanned, P1
Per-task / per-engagement ACLplanned, P1
Vietnamese localisationplanned, P1
Dependency graph + cycle detectionplanned, P1
Custom workflow per projectplanned, P1+
Mobile (offline-first)planned, P3+
Client-visible PORTAL viewplanned, P2+

References

Personas & skill bundles that touch PROJ

PROJ is the work-tracking surface most CUO personas read from and write to. The catalog spans 47 personas with 220+ workflows and their paired author/audit skill bundles; the ones below are PROJ-affined in particular.

Persona affinities (8 of 47):

Skill-bundle reads & writes:

Previous module: EMAIL | Next module: TIME

Changelog

History lives in the changelog; this page describes only the current state.


Generated from services/proj/docs/index.md — edit the markdown source, not this file (TASK-DOCS-002).