Skip to content
Merged
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 .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "agentlinkops",
"version": "0.6.11",
"version": "0.6.12",
"description": "Connect an agent to AgentLinkOps and run backlink work with it: the connect skill (views, the three discovery verbs, evidence rules), local ledger and campaign skills, a portable CRM, the source-cited content pipeline, user-managed disavow lists, HTML reports and monitoring-event sync.",
"author": {
"name": "AgentLinkOps"
Expand Down
2 changes: 1 addition & 1 deletion .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "agentlinkops",
"version": "0.6.11",
"version": "0.6.12",
"description": "Connect an agent to AgentLinkOps and run backlink work with it: the connect skill (views, the three discovery verbs, evidence rules), local ledger and campaign skills, a portable CRM, the source-cited content pipeline, user-managed disavow lists, HTML reports and monitoring-event sync.",
"author": {
"name": "AgentLinkOps"
Expand Down
9 changes: 4 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
# One tagged version reaches npm from here, with provenance, under the AgentLinkOps identity:
# push a tag `vX.Y.Z` that matches package.json, and this workflow tests, packs and publishes
# through npm trusted publishing (OIDC; no token in the repository). The GitHub release is
# created from the tag with the CHANGELOG entry. `workflow_dispatch` with dry_run rehearses
# everything except the final upload.
# Releases are qualified, packed and published locally under the owner deployment policy.
# The retained workflow is disabled so pushing a tag cannot build or publish a second artifact.
name: Release
on:
push:
Expand All @@ -18,6 +15,8 @@ permissions:
id-token: write
jobs:
release:
# Owner policy: qualify and publish locally; tags must not launch a release job.
if: ${{ false }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
All notable changes to the `agentlinkops` package are recorded here. The version follows the
plugin manifests in `.claude-plugin/` and `.codex-plugin/`.

## 0.6.12 (2026-10-07)

- Check every watch import result before updating sync mappings or reporting progress. Reject incomplete, duplicate or out-of-range results without resending the request.
- Show inspection and read-only cursor recovery steps after a sync timeout or lost connection. Keep the project ledger and history while the write outcome is unknown.
- Test multi-batch imports and uncertain responses in the assembled CLI package.

## 0.6.11 (2026-10-06)

- Keep `check --json` stdout limited to observation rows. Send empty-ledger, not-due and filter notices to stderr.
Expand Down
2 changes: 1 addition & 1 deletion cli/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export {CloudError} from './cloud-error.js';
// A small HTTP client for the cloud. Scoped keys only; no human session ever reaches here.
// DP-0036-T13: the CLI names itself so the usage counters can tell it from other REST callers.
// The version follows the plugin manifests; bump it with them.
export const CLI_VERSION = '0.6.11';
export const CLI_VERSION = '0.6.12';
export const CLI_USER_AGENT = `agentlinkops-cli/${CLI_VERSION}`;
const MAX_ATTEMPTS = 3;
const MAX_RETRY_WAIT_MS = 5_000;
Expand Down
7 changes: 7 additions & 0 deletions cli/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,13 @@ export async function main(argv = process.argv.slice(2), { cwd = process.cwd(),
err(JSON.stringify({error: {code: error.code, status: error.status, next: 'Keep this project ledger and its history. Resolve authorized read access for the configured project, then retry agentlinkops sync --pull-only --recover-cursors. Do not broaden grants or replay remote writes.'}}));
return 2;
}
if (command === 'sync' && error instanceof CloudError && error.status === 0 &&
['CLOUD_TIMEOUT', 'CLOUD_NETWORK_ERROR'].includes(error.code)) {
// A local transport failure cannot establish whether a write committed.
// Print trusted recovery guidance, never attached exception/server text.
err(`agentlinkops: ${error.code}\nThe request outcome is unknown. Check the operation result before sending it again. Keep this project ledger and its history. Inspect accessible watches and their local references with agentlinkops sync --pull-only --recover-cursors. Do not broaden grants or replay remote writes.`);
return 2;
}
err(`agentlinkops: ${error?.message ?? error}`);
return 2;
} finally {
Expand Down
4 changes: 2 additions & 2 deletions cli/mcp-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ function retryDelay(error, attempt, now) {

async function sdkClient({url, token, workspaceId, fetchImpl}) {
const {Client, StreamableHTTPClientTransport} = await import('@modelcontextprotocol/client');
const client = new Client({name: 'agentlinkops-cli', version: '0.6.11'});
const client = new Client({name: 'agentlinkops-cli', version: '0.6.12'});
const destination = new URL(url);
const transport = new StreamableHTTPClientTransport(destination, {
authProvider: {token: async () => token},
Expand All @@ -71,7 +71,7 @@ async function sdkClient({url, token, workspaceId, fetchImpl}) {
if (target.href !== destination.href) fail('OAUTH_TOKEN_DESTINATION_MISMATCH');
const headers = new Headers(init.headers ?? (input instanceof Request ? input.headers : undefined));
if (workspaceId) headers.set('X-Workspace-ID', workspaceId);
headers.set('User-Agent', 'agentlinkops-cli/0.6.11');
headers.set('User-Agent', 'agentlinkops-cli/0.6.12');
const response = await fetchImpl(input, {...init, headers, redirect: 'error', signal: AbortSignal.timeout(30000)});
if (response.redirected || response.status >= 300 && response.status < 400
|| response.url && response.url !== destination.href) fail('OAUTH_TOKEN_DESTINATION_MISMATCH');
Expand Down
25 changes: 22 additions & 3 deletions cli/sync.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,24 @@ import { CloudError } from './client.js';

const PUSH_BATCH = 100;

function completeImportRows(result, expected) {
const record = value => value !== null && typeof value === 'object' && !Array.isArray(value);
const text = value => typeof value === 'string' && value.trim().length > 0;
const refuse = () => { throw new CloudError('INVALID_IMPORT_RESPONSE', 502, null,
'The cloud returned an incomplete or invalid watch import result. Some watches may already have been stored. Inspect accessible watches and their local references before retrying sync; this request was not automatically replayed.'); };
if (!record(result) || !Array.isArray(result.rows) || result.rows.length !== expected) refuse();
const seen = new Set();
for (const row of result.rows) {
if (!record(row) || !Number.isInteger(row.index) || row.index < 0 || row.index >= expected || seen.has(row.index)) refuse();
seen.add(row.index);
const watch = Object.hasOwn(row, 'watch'), error = Object.hasOwn(row, 'error');
if (watch === error) refuse();
if (watch && (!record(row.watch) || !text(row.watch.id) || typeof row.watch.created !== 'boolean')) refuse();
if (error && (!record(row.error) || !text(row.error.code) || !text(row.error.message))) refuse();
}
return result.rows;
}

export function syncPlan(entries, state, { includeWanted = false, ledgerIds = null } = {}) {
if (ledgerIds !== null && (!Array.isArray(ledgerIds) || ledgerIds.some(id => typeof id !== 'string') || ledgerIds.some(id => !entries.some(entry => entry.id === id))))
throw new CloudError('INVALID_SYNC_SELECTION', 0);
Expand All @@ -38,12 +56,13 @@ export async function pushExpectations(client, entries, state, { projectId, onPr
for (let index = 0; index < watched.length; index += PUSH_BATCH) {
const batch = watched.slice(index, index + PUSH_BATCH);
const result = await client.importWatches(projectId, batch.map(toWatchInput));
for (const row of result.rows ?? []) {
// Validate the complete acknowledgement before adopting any of this batch.
// Earlier POSTs may have committed; a malformed response never authorizes a replay.
for (const row of completeImportRows(result, batch.length)) {
const entry = batch[row.index];
if (!entry) continue;
if (row.error) { failed.push({ id: entry.id, code: row.error.code, message: row.error.message }); continue; }
mapping[entry.id] = row.watch.id;
if (row.watch.created !== false) created.push(entry.id);
if (row.watch.created) created.push(entry.id);
}
onProgress?.(Math.min(index + PUSH_BATCH, watched.length), watched.length);
}
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "agentlinkops",
"version": "0.6.11",
"version": "0.6.12",
"description": "Backlink checks with dated evidence and portable results, optional repository ledgers, supplier-export imports and sync to AgentLinkOps.",
"author": "AgentLinkOps (https://agentlinkops.com)",
"license": "Apache-2.0",
Expand Down
2 changes: 1 addition & 1 deletion skills/agentlinkops-connect/references/agentlinkops.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ records. This page is the complete agent reference: the same text prints from
full once per session, then use `describe` (or `exec describe` on MCP) for any command's exact schema.

<!-- generated:skill-version begin (npm run generate:tools) -->
Pack version 0.6.11. 139 commands in 15 toolsets.
Pack version 0.6.12. 139 commands in 15 toolsets.
<!-- generated:skill-version end -->

## Install and connect (once per machine)
Expand Down
Loading
Loading