Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,18 +46,21 @@ Run what you build before calling it done. **If you can't run it, say so.**
## PR discipline

- Paste the full PR URL, not the number.
- Record it through the declared surface too: prose URLs are inert, so use
`@agency:pr.link(url: "<full URL>", item: "<item id>")` or the `pr:` field on
`items.state` when shipping an item.
- When the change lands on master, close the PR and delete the branch in the same breath
(`gh pr merge --delete-branch`). An open PR whose content already shipped reads as
unfinished work.

## Closing an item you were given

Reply checkboxes edit the project's item list: `- [ ]` proposes, `- [x] <exact title>`
closes, `- [-] <exact title>` strikes an obsolete row. Titles match exactly and
case-insensitively, so a paraphrase silently appends a near-duplicate. Full contract:
[`docs/task-manager.md`](docs/task-manager.md#the-project-session-contract-three-checkboxes).
Read that, not `projects.rs`: the verbs arrived late, and an older tree reads as
append-only. [`wt-tools`](crates/wt-tools) reads the list and never writes.
The project prompt supplies item ids and the declared Prompt Syntax surface. Report
state with `@agency:items.state`, create with `@agency:items.add`, and remove an
incorrect row with `@agency:items.retire`. Never address an existing row by title:
paraphrases were how the old checkbox contract created near-duplicates. Full contract:
[`docs/task-manager.md`](docs/task-manager.md#the-project-session-contract).
[`wt-tools`](crates/wt-tools) reads the list and never writes.

- **Close in the same turn the work ships.** Shipped means merged and released.
- **Read the title before striking it.** `wt-tools search-items <word>` prints it as stored.
Expand Down
17 changes: 7 additions & 10 deletions AgencyZero.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,18 @@
3. Ship through a pull request.
4. Merge only after review passes, or the owner overrides.
5. No em dashes.
6. Track the work as items, in your reply, as it happens:
`- [ ]` opens, `- [~]` plans, `- [/]` starts, `- [>] title (#35)` ships.
`- [?]` when you are stuck on an answer only the owner can give.
Never `- [x]`: you cannot confirm your own fix works. The owner closes it.
Quoted or fenced checklists are never read as items, so quote freely.
7. To retire a row, or to write into another project, ask the host directly:
`<ps @agency:items.inject(project: "ui")>` … checkbox lines … `</ps>`.
Project by name or by id, omit it to mean this one. Prose can open, plan,
start and ship a row here; only a directive can delete or reach elsewhere.
8. Report item state by id, on its own line, as it happens:
6. Track work through the declared Prompt Syntax surface, in your reply, as it
happens. Prose, checkboxes, quotes, fences and URLs are inert.
7. Report item state by id, on its own line:
`<ps @agency:items.state(id: "item-a3f9", status: "active")>`
`<ps @agency:items.add(ref: "t1", title: "One line", status: "planning")>`
`<ps @agency:items.retire(id: "item-a3f9")>` removes one that should not
be there. The turn's prompt lists the open items and their ids, and answers
back with what each directive did. Never address a row by its title.
8. A pull request is state only when authored as state:
`<ps @agency:pr.link(url: "https://github.com/owner/repo/pull/35", item: "item-a3f9")>`.
Also paste the full URL in prose so the owner can open it. A run started from
an item ships with `items.state(..., status: "shipped", pr: "<full URL>")`.
9. Ask before installing anything: a download, a global cache, a browser, a
toolchain. A repo doc recommending it is not permission.
10. Persisted data goes in a WorkTable table. Not a JSON file beside the store,
Expand Down
14 changes: 7 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ members = [
]

[workspace.package]
version = "0.1.55"
version = "0.1.56"
edition = "2024"
publish = false

Expand Down
2 changes: 1 addition & 1 deletion apps/gui/frontend/src/features/settings/SettingsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ export function SettingsTab(): JSX.Element {
</Row>
<Row
label="Completed items"
hint="what a session's own “done” report does to the row"
hint="what marking an item finished does to the row"
>
<PillMenu
label="Completed items"
Expand Down
4 changes: 2 additions & 2 deletions apps/gui/frontend/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -477,8 +477,8 @@ export interface GlobalSettings {
forwardProxyVars: boolean;
notifications: NotificationSettings;
/**
* What a session's `- [x]` does to an existing item: `"resolve"` marks it
* finished and keeps the row, `"delete"` removes it outright.
* What the owner's finished action does to an existing item: `"resolve"`
* keeps the finished row, while `"delete"` removes it outright.
*/
completedItems: "resolve" | "delete";
/** How the workspace is coloured. See {@link ThemeSettings}. */
Expand Down
2 changes: 1 addition & 1 deletion apps/gui/src/db/schema/pull_request.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Pull requests cut during a project's runs, tracked as chips over the composer.
//!
//! One row per PR URL per project, upserted when a reply mentions the URL and
//! One row per PR URL per project, created from an authored PS directive and
//! refreshed through `gh` when available. `state` is GitHub's own word
//! (`OPEN` | `MERGED` | `CLOSED`, or `unknown` before the first refresh);
//! `ci` is the check rollup reduced to `pass` | `fail` | `pending` | `none` |
Expand Down
98 changes: 79 additions & 19 deletions apps/gui/src/directives.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
//!
//! These are Prompt Syntax spans, and PS's inert-content rule applies: only
//! text the agent authored is parsed, never a quoted or fenced example. The
//! caller is responsible for that (see `items_from_reply`), which is why this
//! caller is responsible for that (see `apply_directives`), which is why this
//! module reads one line at a time and knows nothing about fences.

/// The authoring surface this application declares, per Prompt Syntax 13.2.
Expand All @@ -47,27 +47,22 @@ pub struct Surface {
pub reserved: &'static [&'static str],
/// 13.2.3. The reach, written down rather than implied by a parameter.
///
/// `items.inject` takes a project, so the surface can write outside the
/// project the turn came from. That is deliberate and is the whole reason
/// this field exists: cross-scope reach must be part of the declared bound,
/// never left implicit in what a verb happens to do.
/// `items.add(project: ...)` can write outside the project the turn came
/// from. Home may also create the explicitly named project. Item ids are
/// installation-wide. That reach is deliberate and must be declared,
/// never left implicit in verb semantics.
pub bound: &'static str,
}

/// What this build promotes. Referenced from the prompt, published as YAML.
pub const SURFACE: Surface = Surface {
delimiter: "<ps …> on its own line, outside fenced or quoted content",
namespace: "agency",
verbs: &[
"items.state",
"items.add",
"items.retire",
"items.inject",
"pr.link",
],
verbs: &["items.state", "items.add", "items.retire", "pr.link"],
reserved: &["status:finished", "status:canceled"],
bound: "any project in this installation's store, named by id or by name; \
no reach outside it, and no other namespace is live",
Home Task Manager may create a named project inside that store through \
items.add; no reach outside it, and no other namespace is live",
};

/// The statuses an agent may set.
Expand Down Expand Up @@ -97,11 +92,24 @@ pub enum Directive {
/// module exists to remove.
ItemAdd {
handle: Option<String>,
/// Another project in this installation, by id or exact name.
///
/// Used by Home's task manager so it speaks the same item language as
/// an ordinary project instead of switching to a JSONL dialect.
project: Option<String>,
title: String,
status: String,
},
/// Attach a pull request to an item.
PrLink { number: String, item: String },
/// Track a pull request, and optionally attach it to an item.
///
/// A URL creates the PR row. A number is enough only when an item is also
/// named, because a bare number says nothing about which repository owns
/// it. Keeping both forms lets an already tracked PR be attached cheaply.
PrLink {
url: Option<String>,
number: Option<String>,
item: Option<String>,
},
/// Remove a row, by id.
///
/// The cleanup verb, and the reason it exists is that the old one deleted
Expand Down Expand Up @@ -207,6 +215,9 @@ pub fn parse(line: &str) -> Option<Directive> {
handle: arg(args, "ref")
.map(str::to_string)
.filter(|handle| !handle.is_empty()),
project: arg(args, "project")
.map(str::to_string)
.filter(|project| !project.is_empty()),
title: title.to_string(),
status: arg(args, "status")
.unwrap_or("new")
Expand All @@ -219,10 +230,19 @@ pub fn parse(line: &str) -> Option<Directive> {
return (!id.is_empty()).then(|| Directive::ItemRetire { id: id.to_string() });
}
if verb.eq_ignore_ascii_case("@agency:pr.link") {
let number = arg(args, "number")?.trim_start_matches('#').to_string();
let item = arg(args, "item")?.to_string();
return (!number.is_empty() && !item.is_empty())
.then_some(Directive::PrLink { number, item });
let url = arg(args, "url")
.map(str::to_string)
.filter(|url| !url.is_empty());
let number = arg(args, "number")
.map(|number| number.trim_start_matches('#').to_string())
.filter(|number| !number.is_empty());
let item = arg(args, "item")
.map(str::to_string)
.filter(|item| !item.is_empty());
// A URL can be tracked without an item. A number alone cannot: it has
// no repository and therefore cannot identify a pull request row.
return (url.is_some() || (number.is_some() && item.is_some()))
.then_some(Directive::PrLink { url, number, item });
}
None
}
Expand Down Expand Up @@ -279,12 +299,50 @@ mod tests {
parse(r#"<ps @agency:items.add(ref: "t1", title: "Wrap it, then ship it")>"#),
Some(Directive::ItemAdd {
handle: Some("t1".into()),
project: None,
title: "Wrap it, then ship it".into(),
status: "new".into(),
})
);
}

#[test]
fn an_item_add_may_name_another_project() {
assert_eq!(
parse(
r#"<ps @agency:items.add(project: "Prompt Syntax", ref: "t1", title: "Unify the surface", status: "planning")>"#
),
Some(Directive::ItemAdd {
handle: Some("t1".into()),
project: Some("Prompt Syntax".into()),
title: "Unify the surface".into(),
status: "planning".into(),
})
);
}

#[test]
fn a_pr_url_may_be_tracked_with_or_without_an_item() {
let url = "https://github.com/pathscale/agencyzero/pull/76";
assert_eq!(
parse(&format!(r#"<ps @agency:pr.link(url: "{url}")>"#)),
Some(Directive::PrLink {
url: Some(url.into()),
number: None,
item: None,
})
);
assert_eq!(
parse(r#"<ps @agency:pr.link(number: 76, item: "item-a3f9")>"#),
Some(Directive::PrLink {
url: None,
number: Some("76".into()),
item: Some("item-a3f9".into()),
})
);
assert!(parse(r#"<ps @agency:pr.link(number: 76)>"#).is_none());
}

/// Casual capitalisation compiles to the canonical verb; a confusable does
/// not bind at all, because the fold is ASCII.
#[test]
Expand All @@ -297,6 +355,8 @@ mod tests {
fn anything_that_is_not_ours_is_not_a_directive() {
assert!(parse("<ps @file:glossary.md>").is_none());
assert!(parse(r#"<ps @agency:items.destroy(id: "a")>"#).is_none());
assert!(parse(r#"<ps @agency:items.inject(project: "ui")>"#).is_none());
assert!(parse("- [ ] A checklist is display text").is_none());
assert!(parse("Mention @agency:items.state in a sentence").is_none());
// Missing the fields it exists to carry.
assert!(parse(r#"<ps @agency:items.state(status: "new")>"#).is_none());
Expand Down
Loading
Loading