Skip to content
Open
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
2 changes: 1 addition & 1 deletion docs/docs/configure/skills.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ altimate-code workspace review status # changes nothing on the server; --j

Requests sent from the CLI are recorded as such in the request's history. What the team gets is the exact version that was approved: editing the skill afterwards changes nothing for them until you run `skill publish` again, which submits the new version as an update. A member publishing to a workspace whose owner has review off is refused before anything is uploaded. If the skill uploads but cannot be sent (offline, an expired sign-in, too many open requests), the command says so and exits non-zero; running it again updates the uploaded skill and submits it. Against a workspace service without review, `skill publish` attaches the skill directly as before, which only the workspace owner can do. With `--json`, the output carries `ok`, the upload (`action`, `public_id`, `files`, `bytes`, `workspace_id`, `workspace_name`) and `review` with the service's `status`, `attached_directly`, `duplicate`, `owner_name` and `contribution` (`null` without review); every failure exits non-zero with `{"ok": false, "error": ...}`.

When the owner or an admin approves or rejects one of your requests that this client has seen pending (submitted from it, or listed by an earlier check), the TUI shows a notice once in each project linked to that workspace (with the reason for a rejection). Each altimate-code process with a linked project open checks at start and every 5 minutes (a server without review is asked again only after 6 hours) and keeps the 50 most recent decisions in your own state directory, so a decision that arrives while the TUI is closed or disconnected is shown when it next starts (as rule review notices are). The first time a TUI opens a project, decisions already kept are recorded without a notice; `workspace review status` lists them. `altimate-code workspace review status` lists your requests on the linked workspace (skills and knowledge bases): kind, name, status, whether it is new or an update, the reviewer and their reason, and `expired` for a request waiting longer than 14 days (it can still be approved). In the web app you can withdraw a request that is still waiting, and remove an approved skill or knowledge base of yours from the workspace; the owner or an admin can remove it too, with a reason. When the owner or an admin removes one of yours that this client has seen approved (or waiting), the TUI shows that once too, with the reason; your own removal and withdrawal show nothing. `workspace review status` lists them as `withdrawn` and `removed from the workspace`.
When the owner or an admin approves or rejects one of your requests that this client has seen pending (submitted from it, or listed by an earlier check), the TUI shows a notice once in each project linked to that workspace (with the reason for a rejection). Each altimate-code process with a linked project open checks at start and every 5 minutes (a server without review is asked again only after 6 hours) and keeps the 50 most recent decisions in your own state directory, so a decision that arrives while the TUI is closed or disconnected is shown when it next starts (as rule review notices are). The first time a TUI opens a project, decisions already kept are recorded without a notice; `workspace review status` lists them. `altimate-code workspace review status` lists your requests on the linked workspace (skills and knowledge bases): kind, name, status, whether it is new or an update, the reviewer and their reason, and `expired` for a request waiting longer than 14 days (it can still be approved). In the web app, or from `/rules` in the TUI (which lists your skill and knowledge requests under your rule proposals), you can withdraw a request that is still waiting and remove an approved skill or knowledge base of yours from the workspace; in the web app the owner or an admin can remove it too, with a reason. The VS Code extension does the same through `altimate-code serve` (`GET /altimate/workspace/requests`, `POST /altimate/workspace/requests/{id}/withdraw|remove`), and Claude Code and Codex sessions are told of decisions, and of your requests still waiting, when they start. When the owner or an admin removes one of yours that this client has seen approved (or waiting), the TUI shows that once too, with the reason; your own removal and withdrawal show nothing. `workspace review status` lists them as `withdrawn` and `removed from the workspace`.

`skill publish` sends the whole skill directory, not just `SKILL.md`, so keep secrets out of it. A built-in filter skips known file and directory names — `.env*`, `.git`, `id_rsa`, `*.pem`, `*.key`, `*.p12`, `.npmrc`/`.netrc`, `credentials.json`, `secrets.*`, `.ssh`/`.aws`, editor swap files — but it matches names only and never scans file contents, so a token inside `config.yaml` or a key named `server.crt` would still be uploaded. Built-in skills, global skills and skills the workspace itself sent you cannot be published — including the copies the VS Code / Cursor extension delivers into `.claude/skills/` and `.agents/skills/`, which carry an `.altimate-managed.json` marker.

Expand Down
2 changes: 1 addition & 1 deletion docs/docs/usage/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ With workspaces on:
- `/integrations` in the TUI opens the workspace's integration status directly (the same view as `/workspace` → Status). In a project that is not linked it offers the link, and when the workspace engine is missing or too old it offers the install and says which.
- In a project linked to a workspace, integrations come only from the workspace's engine, so the `datamate_manager` tool is not offered there and refuses to run. `datamate-*` MCP servers already in the config still load, and a one-time notice names the file they are in; the config is not edited.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: the new workspace review decisions command is undocumented

This paragraph documents the review CLI and now mentions /rules and the serve routes, but not altimate-code workspace review decisions --json [--unseen] [--mark-seen] [--host H] that this PR adds for the Claude Code plugin, nor its deliberate always-JSON/always-exit-0 contract (which differs from the general "ok is true exactly when the exit code is 0" statement above). Add it so the CLI reference matches the implementation.

- `altimate-code skill publish <name>` uploads a project skill to the linked workspace, attached directly for the workspace owner or an admin and sent for review for a member when review is on; see [Skills](../configure/skills.md#cli-commands).
- When the owner or an admin approves or rejects one of your skill or knowledge base requests that altimate-code has seen pending (submitted from it, or listed by an earlier check), the TUI shows a notice once in each project linked to that workspace, with the reason for a rejection. Every altimate-code process that opens a linked project checks at start and every 5 minutes while workspaces are on and you are signed in: it lists your own requests on the workspace (up to 10 pages of 50); a server without review is asked again only after 6 hours. The decision is kept per account and workspace in your own state directory (`$XDG_STATE_HOME/altimate-code/workspace-reviews/`, the 50 most recent), so a TUI that was closed or disconnected when it arrived shows it when it next starts. The first time a TUI opens a project, decisions already kept are recorded without a notice; `altimate-code workspace review status` lists them. Withdrawing a waiting request and removing an approved one happen in the web app; when the owner or an admin removes one of yours from the workspace, the TUI shows that once too, with the reason.
- When the owner or an admin approves or rejects one of your skill or knowledge base requests that altimate-code has seen pending (submitted from it, or listed by an earlier check), the TUI shows a notice once in each project linked to that workspace, with the reason for a rejection. Every altimate-code process that opens a linked project checks at start and every 5 minutes while workspaces are on and you are signed in: it lists your own requests on the workspace (up to 10 pages of 50); a server without review is asked again only after 6 hours. The decision is kept per account and workspace in your own state directory (`$XDG_STATE_HOME/altimate-code/workspace-reviews/`, the 50 most recent), so a TUI that was closed or disconnected when it arrived shows it when it next starts. The first time a TUI opens a project, decisions already kept are recorded without a notice; `altimate-code workspace review status` lists them. Withdrawing a waiting request and removing an approved one happen in the web app or from `/rules` in the TUI, which lists your skill and knowledge requests with your rule proposals; when the owner or an admin removes one of yours from the workspace, the TUI shows that once too, with the reason.
- Workspace skills are synced at the version the workspace serves you: the approved version, or your own pending version of a skill you submitted. The next sync follows what the workspace serves: a skill whose first submission is rejected, or that is detached from the workspace, is removed from the project; when an update to an approved skill is rejected or replaced, the skill stays at its approved version. Each skill is downloaded as one version: if it is approved or rejected while its files are being read, nothing of it is installed from that sync and it is fetched again on the next one. When a download fails, the copy already in the project is kept only if it is still the version the workspace serves, as far as the server has said (its list, or a read refused because the version just changed), or if neither the copy nor the list names a version (a copy synced before revisions, or an older server), in which case it is kept until the server says otherwise; a copy of a version it no longer serves (for example your rejected pending version), or of a skill it no longer lists, is removed until the download succeeds, even when no other skill could be downloaded. Two processes syncing the same project take turns: a sync never replaces or removes a snapshot another process installed after it started, and the next sync starts from the newer one.
- When the linked workspace has knowledge, the agent is told to check it for best practices and team conventions before answering, without you having to mention the knowledge base.
- In an ordinary session the agent is told every turn which workspace the project is linked to — or that none is, or that the link could not be verified just now. In an extension-pinned session it is told the pinned workspace instead, and that it differs from the project's own link. Either way "which workspace am I in?" has an answer, and the agent is told not to confuse it with a Databricks workspace or an IDE workspace folder.
Expand Down
89 changes: 88 additions & 1 deletion packages/opencode/src/altimate/rules/host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,36 @@ export interface Start {
rules: { text: string; applies_to: string[]; review?: "pending" }[]
proposals: number
notices: RulesStore.Notice[]
/** Decisions on the person's skill and knowledge requests to the workspace since the last host session (approved,
* rejected, removed from the workspace). */
review_decisions?: ReviewDecision[]
/** How many of the person's skill and knowledge requests wait for review, as last seen. */
review_waiting?: number
/** The last check for proposed rules failed (no model available, …): said once, so the person can fix it. */
check_failed?: string
/** "Remember … as a team rule" requests from a prompt that were not stored: said once. */
unstored?: RulesStore.State["host_unstored"]
}

/** A skill or knowledge decision as a host session start reports it. */
export interface ReviewDecision {
kind: "skill" | "knowledge"
public_id: string
name: string
status: string
reason?: string
reviewer?: string
at: string
}

/** How long a session start waits for the review poll (skill and knowledge decisions) before answering from what was
* kept: the poll is the only way a host-only person learns of them. */
export const REVIEW_POLL_WAIT_MS = 3_000

/** The key a host session start remembers a skill or knowledge decision by. */
export const reviewSeenKey = (decision: Pick<ReviewDecision, "kind" | "public_id" | "status">) =>
`${decision.kind}:${decision.public_id}:${decision.status}`

/** How long a session start waits for a pull before answering from the last one. */
export const START_WAIT_MS = 3_000
/** A project with no pulled rules yet waits longer: without them, nothing else here runs. */
Expand Down Expand Up @@ -159,12 +183,19 @@ export async function start(input: {
const failed = state.last_check?.outcome.startsWith("failed") && isHostSession(state.last_check.session) &&
!seen.has(RulesStore.hostCheckKey(state.last_check)) ? state.last_check : undefined
const unstored = state.host_unstored
// Skill and knowledge decisions: one bounded poll (nothing else polls in a host-only setup), then what is kept.
const review = await within(reviewForHost(input.directory, new Set(state.host_review_seen), Math.min(left(), REVIEW_POLL_WAIT_MS)), {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The new review poll remains behind the rulesEnabled early return, so disabling team-rule learning prevents host sessions from fetching or reporting skill and knowledge review decisions. Run the review path independently of the learning gate, while retaining the workspace/link checks for the data it can access.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/opencode/src/altimate/rules/host.ts, line 187:

<comment>The new review poll remains behind the `rulesEnabled` early return, so disabling team-rule learning prevents host sessions from fetching or reporting skill and knowledge review decisions. Run the review path independently of the learning gate, while retaining the workspace/link checks for the data it can access.</comment>

<file context>
@@ -159,12 +183,19 @@ export async function start(input: {
     !seen.has(RulesStore.hostCheckKey(state.last_check)) ? state.last_check : undefined
   const unstored = state.host_unstored
+  // Skill and knowledge decisions: one bounded poll (nothing else polls in a host-only setup), then what is kept.
+  const review = await within(reviewForHost(input.directory, new Set(state.host_review_seen), Math.min(left(), REVIEW_POLL_WAIT_MS)), {
+    decisions: [] as ReviewDecision[],
+    waiting: 0,
</file context>

decisions: [] as ReviewDecision[],
waiting: 0,
})
// Marked seen only if the write lands in time: otherwise they are said again next time, never lost.
if (input.session && (notices.length || failed || unstored.length))
if (input.session && (notices.length || failed || unstored.length || review.decisions.length))
await within(
RulesStore.update(input.root, (s) => {
const keys = [...notices.map(RulesStore.hostNoticeKey), ...(failed ? [RulesStore.hostCheckKey(failed)] : [])]
s.host_notices_seen = [...s.host_notices_seen, ...keys.filter((key) => !s.host_notices_seen.includes(key))]
const reviewKeys = review.decisions.map(reviewSeenKey)
s.host_review_seen = [...s.host_review_seen, ...reviewKeys.filter((key) => !s.host_review_seen.includes(key))]
s.host_unstored = s.host_unstored.filter((entry) => !unstored.some((said) => said.at === entry.at && said.text === entry.text))
}).catch((error) => log.warn("host notices not marked seen", { error: errorText(error) })),
undefined,
Expand All @@ -178,11 +209,48 @@ export async function start(input: {
})),
proposals: (await within(RuleProposal.storedFor(input.root), [])).length,
notices,
...(review.decisions.length ? { review_decisions: review.decisions } : {}),
...(review.waiting ? { review_waiting: review.waiting } : {}),
...(failed ? { check_failed: failed.outcome.replace(/^failed:\s*/, "") } : {}),
...(unstored.length ? { unstored } : {}),
}
}

/** The person's skill and knowledge decisions not reported to a host yet (within `NOTICE_MAX_AGE_MS`), and how many of
* their requests wait. Polls the server once, within `waitMs`; past it (offline, a slow server), what was kept is used.
* Never throws. */
export async function reviewForHost(
directory: string,
seen: Set<string>,
waitMs: number,
): Promise<{ decisions: ReviewDecision[]; waiting: number }> {
try {
const { ReviewNotices } = await import("../workspace/review-notices")
await Promise.race([
ReviewNotices.poll(directory).catch(() => undefined),
new Promise((resolve) => setTimeout(resolve, waitMs).unref?.()),
])
const state = await ReviewNotices.storedState(directory)
if (!state) return { decisions: [], waiting: 0 }
const now = Date.now()
const decisions = state.notices
.filter((n) => !seen.has(reviewSeenKey(n)) && !(now - Date.parse(n.at) > NOTICE_MAX_AGE_MS))
.map((n): ReviewDecision => ({
kind: n.kind,
public_id: n.public_id,
name: n.name,
status: n.status,
at: n.at,
...(n.reason ? { reason: n.reason } : {}),
...(n.reviewer ? { reviewer: n.reviewer } : {}),
}))
return { decisions, waiting: state.pending.length }
} catch (error) {
log.warn("host review decisions not read", { error: errorText(error) })
return { decisions: [], waiting: 0 }
}
}

/** `start` as context for the host agent; empty when there is nothing to say. Server text is made printable, and the
* workspace name a bounded, quoted label. */
export function context(start: Start, command = "/altimate-code:rules"): string {
Expand All @@ -191,6 +259,7 @@ export function context(start: Start, command = "/altimate-code:rules"): string
...ruleLines(start, ws),
...proposalLines(start, command),
...noticeLines(start, ws),
...reviewLines(start, ws, command),
...(start.check_failed
? [`The last check of the person's corrections for proposed team rules failed (${printable(start.check_failed)}). It needs a model altimate-code can use: \`altimate learn status\` shows the setting.`]
: []),
Expand Down Expand Up @@ -247,6 +316,24 @@ function noticeLines(start: Start, ws: string): string[] {
return [`Reviews of the person's proposed rules in ${ws} since their last session:`, ...start.notices.map(line)]
}

function reviewLines(start: Start, ws: string, command: string): string[] {
const what = (d: ReviewDecision) => (d.kind === "skill" ? "skill" : "knowledge base")
const line = (d: ReviewDecision) => {
const by = d.reviewer ? ` by ${printable(d.reviewer)}` : ""
const outcome =
d.status === "active" ? `approved${by}; it now reaches the workspace` : d.status === "retired" ? `removed from the workspace${by}` : `${printable(d.status)}${by}`
return `- ${what(d)} "${printable(d.name)}": ${outcome}${d.reason ? ` — ${printable(d.reason)}` : ""}`
}
const lines: string[] = []
if (start.review_decisions?.length)
lines.push(`Decisions on the skills and knowledge bases the person sent to ${ws} for review, since their last session (tell them):`, ...start.review_decisions.map(line))
if (start.review_waiting) {
const one = start.review_waiting === 1
lines.push(`${one ? "One" : start.review_waiting} of the person's skill or knowledge requests ${one ? "is" : "are"} waiting for review in ${ws}; ${command} lists ${one ? "it" : "them"}.`)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: ${command} does not list skill or knowledge requests

context() defaults command to /altimate-code:rules, which in this repo is altimate-code learn proposals (docs/configure/learn.md:200,222) and lists only stored rule proposals and rule rows — not the person's skill/knowledge requests. The line therefore tells the person (and the agent) to run a command that will show nothing about the waiting request. Point at altimate-code workspace review status (or /rules in the TUI) here.

}
return lines
}

/** "Remember this as a team rule: X", "save as a rule - X", "don't forget, team rule: X": a request that names a team rule
* ("as a rule" or "team rule", then ":" or "-"), and what it is. Only explicit wording: "remember to run the tests",
* "do you remember that rule, …?" or "remember this rule: …" is the conversation, not a team rule. The cue words are the
Expand Down
Loading
Loading