Skip to content

Capture decisions

Records are how a project remembers. A decision you record today appears in the brief your next agent session reads. This guide covers writing records directly, replacing them, and the propose-then-publish flow for agent-written records.

All examples assume an onboarded project. See Onboard a repository.

Terminal window
PILOT=/absolute/path/to/pilot.json

Every record has a kind:

brief, map, command, environment, constraint, fact, decision, experiment, unknown, handoff

and a status:

StatusMeaning
observedThe default. Recorded, not independently checked.
confirmedThe owner confirmed it. Requires --reviewer. This is a workflow receipt, not authentication.
disputedSomeone disagrees. Raise it before acting.
proposedWaiting for review. Not part of the brief.

See Records for the full model.

Preview first. Without --apply, nothing is written:

Terminal window
devtwin record add --pilot "$PILOT" \
--scope example --project example.atlas \
--kind decision --title "Use server-side search" \
--body "Filter on the server because the list can grow." \
--request-id req_atlas_decision_01
{"ok":true,"command":"record add","result":{"preview":true,"recordId":"rec_94315938d3e27ab174b5665d","scope":"example","project":"example.atlas"}}

Then write it with the same flags plus --apply:

Terminal window
devtwin record add --pilot "$PILOT" \
--scope example --project example.atlas \
--kind decision --title "Use server-side search" \
--body "Filter on the server because the list can grow." \
--request-id req_atlas_decision_01 --apply
{"ok":true,"command":"record add","result":{"recordId":"rec_94315938d3e27ab174b5665d","recordHash":"sha256:4baf…","created":true,"bodyHash":"sha256:f658…"}}

Keep the recordId and recordHash. You need both to replace the record later.

  • --request-id is required to apply. It has the form req_ plus 8–64 lowercase letters, digits, _ or -. The record ID is derived from the scope, project and request ID, so repeating the same request is safe and returns the existing record.
  • --title is one line, up to 200 characters. The body is up to 32,768 bytes.
  • Text that looks like a credential is refused with DEVTWIN_SENSITIVE_CONTENT.
  • --status confirmed without --reviewer fails with DEVTWIN_APPROVAL_REQUIRED.

--body lands in your shell history. For anything longer or more sensitive, write the body to a file under the pilot’s inputsRoot and pass --body-file:

Terminal window
devtwin record add --pilot "$PILOT" \
--scope example --project example.atlas \
--kind constraint --title "No direct database access from the UI" \
--body-file /absolute/path/to/pilot/inputs/constraint.md \
--request-id req_atlas_constraint_01 --apply

A body file outside inputsRoot is refused with DEVTWIN_GATE_REQUIRED.

Add --source (repeatable, up to 20) so later readers can check the claim. Accepted forms:

FormExample
file:<repo-relative path>file:docs/search.md
commit:<sha>commit:409dcf1
record:<record id>record:rec_94315938d3e27ab174b5665d
issue:<KEY-123>issue:ATL-42
user:<actor>user:email:owner@example.com
url:<https URL>url:https://example.com/adr/7 (no query, fragment or credentials)
Terminal window
devtwin record add --pilot "$PILOT" \
--scope example --project example.atlas \
--kind decision --title "Use server-side search" \
--body "Filter on the server." \
--status confirmed --reviewer email:owner@example.com \
--request-id req_atlas_decision_02 --apply

The reviewer must be an actor reference: email:, github:, slack:, linear: or handle: followed by the identifier.

Records are immutable. To change one, write a successor that supersedes it. Pass the target’s ID and its current hash, so a concurrent change is caught:

Terminal window
devtwin record supersede --pilot "$PILOT" \
--scope example --project example.atlas \
--kind decision --title "Use client-side search for now" \
--body "Small lists; revisit at 10k rows." \
--target rec_94315938d3e27ab174b5665d \
--expect-hash sha256:4bafd4aa… \
--request-id req_atlas_decision_03 --apply

The brief now shows the successor and leaves the superseded record out. History keeps both. If the target was already superseded or its hash changed, you get DEVTWIN_STALE_VERSION with the message Target changed. Read the brief again and supersede the current record instead.

--target and --expect-hash are repeatable and pair up in order, so one successor can replace several records.

When an agent records something, it should not write straight into the brief. It proposes; you publish.

  1. The agent (usually through the devtwin-record skill) writes a private proposal:

    Terminal window
    devtwin record propose --pilot "$PILOT" \
    --scope example --project example.atlas \
    --kind unknown --title "Search threshold" \
    --body-file /absolute/path/to/pilot/inputs/note.md \
    --source file:README.md --request-id req_atlas_prop_01 --apply
    {"ok":true,"command":"record propose","result":{"proposalId":"prop_d4b10a44abfac39728707954","path":"…/data/scopes/example/projects/example.atlas/proposals/prop_d4b10a44abfac39728707954.json"}}

    A proposal is a file, not a record. It does not appear in the brief.

  2. You read the proposal file and decide. To publish it:

    Terminal window
    devtwin record publish --pilot "$PILOT" \
    --scope example --project example.atlas \
    --proposal prop_d4b10a44abfac39728707954 --apply

    Publishing creates an observed record with the original recorder’s provenance. Add --reviewer <actor> to set the review field. Without --apply, record publish only echoes the proposal ID.

If a scope lists a local hub (see --hub in the configuration reference), you can prepare a learning proposal from confirmed records:

Terminal window
devtwin hub propose --pilot "$PILOT" \
--scope example --project example.atlas \
--record rec_640c02d0eeba927643c3d5cf --hub team-notes

Add --apply to write a private proposal file under the project’s proposals/ directory. devtwin never writes to the hub itself. Text that names the scope, project, bound paths or remote hosts is refused with DEVTWIN_SENSITIVE_CONTENT.

Records written through the CLI are attributed to recordedBy: {actor: "human", runId: "run_cli00001"}, whoever runs the command. Treat that label as a placeholder, not an identity. See Limitations.