agentlinkops 0.6.9 → 0.6.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +11 -0
  4. package/README.md +44 -12
  5. package/cli/admission.js +9 -1
  6. package/cli/agent-setup.js +189 -20
  7. package/cli/catalog.js +7938 -1357
  8. package/cli/client.js +89 -23
  9. package/cli/cloud-error.js +21 -0
  10. package/cli/commands.js +15 -3
  11. package/cli/connection.js +74 -12
  12. package/cli/doctor.js +24 -9
  13. package/cli/gsc-browser.js +19 -6
  14. package/cli/ledger.js +4 -7
  15. package/cli/lifecycle.js +5 -2
  16. package/cli/local-result.js +4 -4
  17. package/cli/main.js +42 -27
  18. package/cli/mcp-client.js +198 -0
  19. package/cli/native-lock.js +22 -0
  20. package/cli/oauth-credentials.js +196 -0
  21. package/cli/oauth-flow.js +259 -0
  22. package/cli/setup-plan.js +5 -5
  23. package/cli/status.js +14 -2
  24. package/cli/sync-lock.js +105 -0
  25. package/cli/sync.js +14 -6
  26. package/package.json +6 -3
  27. package/references/cli.md +22 -0
  28. package/references/local-crm-draft-handoff.md +76 -0
  29. package/references/portable-ledger-handoff.md +50 -0
  30. package/references/recipes/catalog.json +9 -9
  31. package/references/recipes/command-notes.md +17 -1
  32. package/references/recipes/placement-reconciliation.md +19 -5
  33. package/references/recipes/qualified-campaign-handoff.md +16 -4
  34. package/references/recipes/site-context-brief.md +2 -2
  35. package/references/recipes/sourced-linkable-asset.md +8 -8
  36. package/scripts/agentlinkops.py +30 -5
  37. package/scripts/outreach-crm-draft.py +121 -0
  38. package/scripts/portable-handoff.py +184 -0
  39. package/shared/lifecycle-contract.js +2 -2
  40. package/shared/response-contract.js +7 -1
  41. package/skills/agentlinkops-assets/SKILL.md +2 -2
  42. package/skills/agentlinkops-campaigns/SKILL.md +3 -3
  43. package/skills/agentlinkops-connect/SKILL.md +29 -6
  44. package/skills/agentlinkops-connect/references/agentlinkops.md +34 -13
  45. package/skills/agentlinkops-crm/SKILL.md +17 -3
  46. package/src/citations/adapter.js +3 -2
  47. package/src/citations/browser/engine.js +23 -7
  48. package/src/citations/login.js +42 -5
  49. package/src/discovery/contract.js +1 -1
  50. package/src/response-shaping.js +58 -0
  51. package/src/tool-views.js +3 -1
@@ -0,0 +1,50 @@
1
+ # Portable ledger handoff
2
+
3
+ Capability: local draft-only file. Native vendor acceptance: untested. No network
4
+ or messages. Workspace/project identities are supplied by the customer and are
5
+ not verified against hosted grants by this helper.
6
+
7
+ Run the CLI's frozen report first:
8
+
9
+ ```sh
10
+ agentlinkops report --json --as-of 2026-09-30T12:00:00Z --out report.json
11
+ python3 scripts/portable-handoff.py --report report.json --mapping mapping.json --out handoff.json
12
+ ```
13
+
14
+ Use your own observation cutoff, rather than copying this example's date.
15
+ `mapping.json` requires version 1, workspace_id, project_id, site (an explicit URL),
16
+ destination `portable-file`, source_report_sha256 and records. Compute the report
17
+ hash from JSON encoded with sorted keys, compact separators and UTF-8; the helper's
18
+ `digest` function defines it. This local command prints the value:
19
+
20
+ ```sh
21
+ python3 -c 'import hashlib,json; r=json.load(open("report.json")); print(hashlib.sha256(json.dumps(r,sort_keys=True,separators=(",",":"),ensure_ascii=False).encode()).hexdigest())'
22
+ ```
23
+
24
+ Each records key is an actual local ledger ID, with
25
+ external_id, reply (`unknown`, `none`, `replied`) and suppression (`unknown`,
26
+ `suppressed`, `clear`). Supply these from your own tools; an empty field never
27
+ means permission to send. Every report row needs an explicit mapping. Unknown
28
+ customer IDs must stay unknown; do not fabricate a mapping to make export pass.
29
+
30
+ The handoff carries the original report state, observation date and evidence block,
31
+ including any hash and locator, plus a stable operation ID. Notes and optional
32
+ commercial references are not copied. `ref` is private by default in the report;
33
+ this helper uses your explicit mapping instead. Identical reruns read the original
34
+ file. A different report, site or existing output refuses before replacement.
35
+ Keep a distinct output for a new snapshot; this is a dated artifact, not a monitor.
36
+
37
+ Read the saved JSON back before claiming persistence. Its destination outcome is
38
+ `not_attempted`, and `send_eligible` remains false on every row. Passing this helper
39
+ does not prove a CRM import or sequencer readback. Follow the
40
+ [qualified campaign handoff](recipes/qualified-campaign-handoff.md) for explicit
41
+ mapping, original-operation reconciliation and destination readback. After a
42
+ response-lost import, preserve its actual operation identity and unknown outcome;
43
+ do not replace it with this file's operation ID or repeat a create to repair an
44
+ artifact error. [Placement reconciliation](recipes/placement-reconciliation.md)
45
+ keeps monitoring facts apart from sent/replied/suppressed records.
46
+
47
+ Package invocation assumes the plugin's root directory. Local source invocation
48
+ uses toolkit/plugin/agentlinkops/scripts/portable-handoff.py. Unsupported native
49
+ vendors, mailbox access and automatic suppression synchronization remain outside
50
+ this helper. No account, credential or send action is installed by it.
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "kind": "agentlinkops_recipe_catalog",
4
- "version": "1.0.5",
5
- "reviewedAt": "2026-09-20",
4
+ "version": "1.0.8",
5
+ "reviewedAt": "2026-10-06",
6
6
  "convention": "AgentLinkOps package convention; not an industry schema",
7
7
  "recipes": [
8
8
  {
9
9
  "id": "site-context-brief",
10
- "version": "1.0.0",
10
+ "version": "1.0.1",
11
11
  "path": "site-context-brief.md",
12
12
  "goals": [
13
13
  "build-content",
@@ -35,7 +35,7 @@
35
35
  },
36
36
  {
37
37
  "id": "sourced-linkable-asset",
38
- "version": "1.0.0",
38
+ "version": "1.0.1",
39
39
  "path": "sourced-linkable-asset.md",
40
40
  "goals": [
41
41
  "build-content"
@@ -62,7 +62,7 @@
62
62
  },
63
63
  {
64
64
  "id": "qualified-campaign-handoff",
65
- "version": "1.0.5",
65
+ "version": "1.0.7",
66
66
  "path": "qualified-campaign-handoff.md",
67
67
  "goals": [
68
68
  "prepare-campaign"
@@ -89,7 +89,7 @@
89
89
  },
90
90
  {
91
91
  "id": "placement-reconciliation",
92
- "version": "1.0.4",
92
+ "version": "1.0.5",
93
93
  "path": "placement-reconciliation.md",
94
94
  "goals": [
95
95
  "verify-links"
@@ -127,21 +127,21 @@
127
127
  "title": "Campaign guidance",
128
128
  "publisher": "AgentLinkOps",
129
129
  "publicationDate": null,
130
- "reviewedAt": "2026-09-13",
130
+ "reviewedAt": "2026-10-06",
131
131
  "url": "../../skills/agentlinkops-campaigns/SKILL.md"
132
132
  },
133
133
  {
134
134
  "title": "Asset guidance",
135
135
  "publisher": "AgentLinkOps",
136
136
  "publicationDate": null,
137
- "reviewedAt": "2026-09-13",
137
+ "reviewedAt": "2026-10-06",
138
138
  "url": "../../skills/agentlinkops-assets/SKILL.md"
139
139
  },
140
140
  {
141
141
  "title": "Recipe command notes",
142
142
  "publisher": "AgentLinkOps",
143
143
  "publicationDate": "2026-09-20",
144
- "reviewedAt": "2026-09-20",
144
+ "reviewedAt": "2026-10-06",
145
145
  "url": "command-notes.md"
146
146
  }
147
147
  ]
@@ -1,9 +1,25 @@
1
1
  # Recipe command notes
2
2
 
3
+ Source review: October 6, 2026. The selection guidance below describes maintained source; package and client release acceptance are separate. Older published versions may lack these flags. Inspect `agentlinkops --help` before using them.
4
+
3
5
  `agentlinkops setup --plan --goal verify-links|prepare-campaign|build-content --mode local|hosted|external` inspects known workspace paths and returns a plan. It does not connect an account or change records.
4
6
 
5
7
  `agentlinkops check --source SOURCE_URL --target TARGET_URL --scope exact --json` checks one supplied pair. Exit zero means it returned a result; read the structured observation to determine presence and evidence limits.
6
8
 
7
9
  A ledger check exits one when an expected link is observed absent with complete evidence. Unknown results do not establish removal. Usage or configuration errors supply no new observation. Other commands and flags have their own exit rules; inspect `agentlinkops --help` and the returned result.
8
10
 
9
- Continue with [placement reconciliation](placement-reconciliation.md) or the [site brief](site-context-brief.md). These notes describe the packaged CLI behavior reviewed on September 20, 2026.
11
+ The plan and check behavior above retains the CLI review from September 20, 2026.
12
+
13
+ ## Optional writing and review guidance
14
+
15
+ Use the customer's chosen source, editorial and review process. Selecting a recipe does not select the packaged content skills. If the customer chooses that pack's writing method, explicitly select each required stage:
16
+
17
+ ```sh
18
+ agentlinkops agent setup --skill source-cited-content-builder --skill source-cited-humanizer --skill content-defingerprinting --skill internal-linking-optimizer --skill pre-publish-review
19
+ ```
20
+
21
+ Alternatively, choose `agentlinkops agent setup --all-skills` when the customer wants the complete pack. Read the selected skills and follow their own rules, including the separate editorial pass and final-text scan. Keep reviews tied to the actual revision and message or page format. A missing or unrun required review leaves that review incomplete; recipe selection, installation or a green build does not pass it.
22
+
23
+ ## Optional continuations
24
+
25
+ Use the customer's own next-step process, or select a packaged continuation separately. Choose `agentlinkops agent setup --recipe placement-reconciliation` before reading [placement reconciliation](placement-reconciliation.md), or `agentlinkops agent setup --recipe site-context-brief` before reading the [site brief](site-context-brief.md). A catalog entry or link does not establish that the recipe is installed.
@@ -1,6 +1,6 @@
1
1
  # Placement reconciliation
2
2
 
3
- ID: `placement-reconciliation` | Version: `1.0.4` | Goal: `verify-links`
3
+ ID: `placement-reconciliation` | Version: `1.0.5` | Goal: `verify-links`
4
4
 
5
5
  Use this recipe to compare claimed placements with observed source pages. Modes: local, hosted, external. Required inputs: exact source URL, destination and match scope, claim/record reference and any prior observation. An exact source page URL is mandatory. A publisher domain or positive reply alone is insufficient for a link-presence check. Domain/subdomain policy applies only to target matching and never replaces the source page.
6
6
 
@@ -26,16 +26,30 @@ For hosted work, first identify the operation in the client's available tools an
26
26
 
27
27
  ## Recovery and completion
28
28
 
29
- For an import or interrupted write, follow [campaign recovery](qualified-campaign-handoff.md#recovery-and-completion). Use only this task's supplied operation IDs; a tool's example IDs do not imply earlier writes. Keep any saved recipe version alongside the version used now. Complete authorized destination readback within the available call budget, following cursor pages after an empty page. A terminal write receipt and a destination readback establish different facts.
29
+ For an import or interrupted write, recover from this task's saved request and receipts. Preserve the destination/workspace/project binding, exact command or method, subject, non-secret arguments, selected records and source/target scope, original idempotency or conditional request identifier, and resulting operation ID. Keep credentials in the customer's existing credential mechanism. A saved key alone does not identify the request; do not change its subject or fill missing arguments from another available ID. Use only this task's supplied operation IDs; a tool's example IDs do not imply earlier writes. If no write was attempted, do not invent an operation to recover. Keep any saved recipe version alongside the version used now; leave its original version unknown when none was recorded.
30
30
 
31
- After a recovered operation read, read the destination again before returning the checkpoint. Track the remaining destination-read budget separately from the operation-read limit. Preserve any actual field mapping supplied by a tool. Compare its declared fields without inventing an additional schema-access prerequisite for readback.
31
+ Complete the following reads before returning the checkpoint, within the available permissions and call budget:
32
+
33
+ 1. Read the original operation using its saved identifier. Restore only authorized access when needed. Check the returned operation ID, destination binding and selected-record scope against the checkpoint. If the original request or identifier is incomplete, use an authorized operation lookup when available and leave unresolved fields unknown; do not schedule a replacement write to manufacture a receipt.
34
+ 2. After that operation read, start a fresh destination lookup at its first page. Follow every cursor returned by this new traversal, including after an empty first page, until the snapshot is complete; leave readback incomplete if permission, budget or traversal limits prevent completion. Account for every affected ID established by the original receipt. Track operation-read, destination-read and total-call budgets separately; exhausting operation reads does not consume the remaining destination-read allowance. Perform those remaining authorized reads before returning.
35
+ 3. Compare returned IDs and field values with the intended revision and actual supplied mapping. A tool's declared field mapping is usable evidence for those fields; do not invent an additional schema-access prerequisite for their readback. Check identity sets, not just row counts, and verify workspace/project binding. Save the original operation receipt, fresh readback and artifact-persistence outcome as separate facts. If a required read cannot finish, name the actual tool, permission, quota or budget limit and the next permitted read.
36
+
37
+ Keep each traversal's snapshot identifier, page sequence and saved items together. Do not reuse a cursor from an earlier snapshot or splice earlier and later pages. A missing, repeated, reversed or expired page leaves the traversal incomplete. After a later authorized repair, restart readback at the first page again. A local save failure, incomplete cursor traversal or unknown link check does not authorize another remote write or prevent otherwise available destination reads.
38
+
39
+ Restored access or an eligible retry time permits another read; it does not establish a terminal result. A pending or unavailable read leaves the write outcome unknown. Record the next permitted read or recovery action and honor supported retry intervals without an unbounded polling loop. Do not claim a wait, restored quota or absolute eligibility time without tool or clock evidence.
40
+
41
+ A terminal receipt and destination readback establish different facts. A failed operation can have partial effects. Use `confirmed not imported` only when the original receipt conclusively establishes no effects for every selected record. Preserve affected IDs and fields separately, then compare current destination values with the intended revision. An empty lookup, including one before a later commit, cannot prove no effects or a failed write. Keep unresolved fields unknown; do not infer read lag or failure from that sequence. A committed receipt does not establish original record creation unless it says so.
42
+
43
+ An attempted write without a conclusive receipt remains `outcome unknown`; use `not attempted` only when no write was attempted. Do not retry an ambiguous create with a fresh request identifier, repeat a write to repair a read or checkpoint failure, or claim that failed authentication establishes a failed write. Any replay must preserve the original request and follow the destination's documented rules; neither restored access nor a failed receipt authorizes replay. A separately chosen repair needs the required write authorization and documented conditional or replay contract. Keep the original operation, provider-issued conditional identifier and any repair operation distinct.
44
+
45
+ Parse canonical JSON results rather than manually rebuilding nested receipts. Use an equivalent authorized CLI result only when it preserves the recorded command and request; do not widen permissions or change credentials to switch surfaces. Write and read back the checkpoint before claiming it saved. Match saved bytes and external IDs to its revision and destination binding using the customer's chosen persistence mechanism. If persistence fails, retain the known operation outcome and destination readback separately, name the failed artifact action and safe resume step, and do not repeat the remote write to repair a local save.
32
46
 
33
47
  Preserve each observation's timestamp and timezone. Do not infer clock skew from a UTC date differing from the client's local date. Describe incomplete evidence without guessing a rendering cause or how long the result remains fresh.
34
48
 
35
- An unknown current check makes no finding about presence or absence. Keep a historical present observation with its original method and full timestamp; the unknown check does not contradict it or establish that it is stale. Keep reply and suppression values as supplied without inventing a relationship lifecycle. Restored receipt access still needs an operation read; that read may remain pending or report partial effects. Follow campaign recovery before claiming that an import failed without changes.
49
+ An unknown current check makes no finding about presence or absence. Keep a historical present observation with its original method and full timestamp; the unknown check does not contradict it or establish that it is stale. Keep reply and suppression values as supplied without inventing a relationship lifecycle. Restored receipt access still needs the original operation read and fresh destination readback above; that operation read may remain pending or report partial effects. Never claim that an import failed without changes unless its receipt establishes the required no-effects proof.
36
50
 
37
51
  After interruption, inspect saved observations and remote records before repeating writes. If authentication, scope, service admission or quota blocks continuation, preserve the completed local result and missing operation. Temporary errors do not prove removal. Do not erase a history gap or delete records to force sync success.
38
52
 
39
- Complete with a dated observation and reconciled claim, or an explicit unknown with its reason and next check. [Campaign handoff](qualified-campaign-handoff.md) retains relationship context.
53
+ Complete with a dated observation and reconciled claim, or an explicit unknown with its reason and next check. Use the customer's chosen process for campaign relationship context. If the customer chooses the optional packaged [campaign handoff](qualified-campaign-handoff.md), select it separately with `agentlinkops agent setup --recipe qualified-campaign-handoff` before reading it; use selectors only when the installed CLI's help supports them. The recovery requirements above do not depend on that continuation.
40
54
 
41
55
  Tested client/tool versions: none recorded. Status: authored; end-to-end execution and vendor connections untested. [Catalog](catalog.json) owns machine-readable status and source references.
@@ -1,22 +1,34 @@
1
1
  # Qualified campaign handoff
2
2
 
3
- ID: `qualified-campaign-handoff` | Version: `1.0.5` | Goal: `prepare-campaign`
3
+ ID: `qualified-campaign-handoff` | Version: `1.0.7` | Goal: `prepare-campaign`
4
4
 
5
5
  Use this recipe to qualify supplied publisher candidates and prepare a draft for the user's CRM or sender. Modes: local, hosted, external. Required inputs: product/audience brief, asset and publication state, candidate records, campaign purpose, chosen destination and the user's outreach constraints.
6
6
 
7
7
  ## Capabilities and ownership
8
8
 
9
- Supplied-input qualification and drafting need no AgentLinkOps account. Fresh page checks need an available browser, verifier or permitted hosted tool. Hosted supplier research depends on actual service access. Any vendor connection is optional and untested here. The CRM owns contacts and relationships; the sender owns delivery, replies and suppression. Use [campaign guidance](../../skills/agentlinkops-campaigns/SKILL.md) for qualification. Its SQLite recording step applies only when the user chooses that store.
9
+ Supplied-input qualification and drafting need no AgentLinkOps account. Fresh page checks need an available browser, verifier or permitted hosted tool. Hosted supplier research depends on actual service access. Any vendor connection is optional and untested here. The customer's chosen files, CRM or other record system owns contacts and relationships; the sender owns delivery, replies and suppression. Use [campaign guidance](../../skills/agentlinkops-campaigns/SKILL.md) for qualification. The bundled local CRM is optional and applies only when the customer chooses that store. Selecting this recipe does not install the optional content skills or require that writing method; use the customer's chosen source, editorial and review process.
10
10
 
11
11
  ## Steps and checkpoints
12
12
 
13
13
  1. Select the campaign mechanism and publisher benefit. Group duplicates using stable record IDs and the exact source/target pair, retaining each supplied origin. Missing origin stays unknown; a duplicate or zero-change import receipt cannot establish a first import. Grouping a handoff does not mean a saved merge. A domain alone needs an exact source page before qualification; destination matching scope cannot substitute for that page.
14
14
  2. Inspect the relevant source page, its context, retrieval time and any submission terms. Label incomplete evidence and browser-rendered evidence with their methods and limits. Verify published contact/form routes; never infer email patterns or deliverability.
15
15
  3. Apply the user's exclusion rules and current reply/suppression evidence. Separate eligible, rejected and unresolved candidates with reasons. If stops cannot be checked, leave the handoff unready for sending.
16
- 4. Read the site's content skill when present. Prepare the draft with [source-cited-content-builder](../../skills/source-cited-content-builder/SKILL.md), then run [source-cited-humanizer](../../skills/source-cited-humanizer/SKILL.md), [content-defingerprinting](../../skills/content-defingerprinting/SKILL.md), [internal-linking-optimizer](../../skills/internal-linking-optimizer/SKILL.md), the site's image workflow when applicable, and [pre-publish-review](../../skills/pre-publish-review/SKILL.md). Apply links and review to the actual message format. Keep claims consistent with the asset's actual publication state. Check the draft itself for claims of reviewing a page, noticing a gap, owning the asset or representing its author. Include such claims only when supplied facts and a dated observation support them. A historical snapshot is not a fresh inspection, and an external warning does not repair an unsupported sentence. Leave unknown identity and factual details unset; do not pull operator identity from session metadata.
16
+ 4. Read the site's applicable content instructions. Prepare and review the draft through the customer's chosen source and editorial process; identify missing required checks before marking it reviewed. If the customer chooses the packaged passes, select them explicitly as described in [optional writing and review guidance](command-notes.md#optional-writing-and-review-guidance), then follow those skills' own rules. Apply links, images when relevant and review to the actual message format. Keep claims consistent with the asset's actual publication state. Check the draft itself for claims of reviewing a page, noticing a gap, owning the asset or representing its author. Include such claims only when supplied facts and a dated observation support them. A historical snapshot is not a fresh inspection, and an external warning does not repair an unsupported sentence. Leave unknown identity and factual details unset; do not pull operator identity from session metadata.
17
17
  5. Inspect the destination's actual field schema before proposing a write, then preview the mapping and changes. Without schema access, return a portable handoff with logical labels, explicitly unmapped; do not present guessed names as CRM fields. Retain campaign references and existing external IDs in supported fields or a companion note. Read back created/updated records and reconcile accepted, rejected and unchanged rows. Exit zero or an empty import is not proof of a useful result. Zero changes does not prove a prior successful import, original source or record creation; preserve those facts only when another receipt establishes them.
18
18
  6. Checkpoint recipe version, source/target references, draft revision, external IDs, readback evidence and next action. On replay, find the saved destination record before creating another. Stop at the draft/handoff boundary; this recipe sends no messages and activates no sequence.
19
19
 
20
+ ## Durable request and readback identity
21
+
22
+ Before attempting a remote write, save the destination/workspace/project binding, command or method, exact non-secret arguments, original idempotency or conditional request identifier, and any resulting operation ID in the checkpoint. Store credentials in the existing credential mechanism, never in the checkpoint. Preserve the request separately from the receipt. A saved key alone does not identify the operation: for example, `request_check` with `subject: "link"` must not be replayed as a target check because a target ID is also available. Use this example only when this task actually recorded that command and subject.
23
+
24
+ On resume, replay only the saved request under the destination's documented replay rules. Do not infer a missing subject, selected record, scope or argument from another ID. If the request is incomplete, use an authorized operation lookup when available and retain the unresolved fields; do not schedule a replacement operation to manufacture a receipt. Verify the returned operation ID and destination binding against the original checkpoint before treating the result as recovered.
25
+
26
+ Persist canonical tool results by parsing their JSON rather than manually copying nested objects. Use the maintained CLI's JSON output when it provides the same authorized command and the MCP client cannot export the result intact. Do not change credentials or widen permissions to switch surfaces. Save the original receipt, parsed readback and local acknowledgement as separate facts.
27
+
28
+ For a resumed paginated read, retain the original page items and traverse only its returned continuation. Before marking it complete, check that saved IDs are unique, each item belongs to the expected workspace/project and the saved items include every affected ID established by the original receipt. Check identity sets, not just row counts: two copies of one row cannot stand in for two different records. If the cursor expires or the snapshot changes, preserve the old partial traversal and start a separately identified fresh read; never splice snapshots. After a repair, use the fresh traversal required below. Keep an incomplete or mismatched page unresolved and preserve the remote receipt; a local persistence failure does not authorize another remote write.
29
+
30
+ Write and read back the checkpoint before reporting it saved. Where the user chooses the maintained ledger or portable helper, use its supported persistence path and retain its concurrency/filesystem limits. Otherwise use the customer's established local persistence mechanism; do not invent a successful lock, atomic save or vendor acknowledgement. Match the saved draft bytes and external IDs to the checkpoint's declared revision and binding.
31
+
20
32
  ## Recovery and completion
21
33
 
22
34
  Before returning an interrupted handoff, complete these steps within the available permissions and call budget:
@@ -43,7 +55,7 @@ Record an attempted write with no conclusive receipt as `outcome unknown`, inclu
43
55
 
44
56
  Treat campaign references as logical labels until the destination's schema and lookup behavior are known. Do not invent a vendor filter from a local field name. If a record is found, compare its saved content and the current customer revision before proposing an update. Neither a timeout nor changed local content establishes which revision reached the destination. Describe supplied file state as supplied evidence; report an inspection, merge or saved change only after performing it.
45
57
 
46
- Complete when the qualified list, reviewed drafts, mapping and destination readback are available. Otherwise return a portable handoff with the actual operation state and remaining action. Retain sent/replied facts from the sender separately from backlink observations. Preserve the supplied suppression value; do not translate replied or suppressed into a permanent relationship closure. Report only the analytics dimensions explicitly described by the data. Later use [placement reconciliation](placement-reconciliation.md).
58
+ Complete when the qualified list, reviewed drafts, mapping and destination readback are available. Otherwise return a portable handoff with the actual operation state and remaining action. Retain sent/replied facts from the sender separately from backlink observations. Preserve the supplied suppression value; do not translate replied or suppressed into a permanent relationship closure. Report only the analytics dimensions explicitly described by the data. Use the customer's chosen reconciliation process later. The packaged [placement reconciliation](placement-reconciliation.md) is an optional, separately selected continuation: choose `agentlinkops agent setup --recipe placement-reconciliation` before reading it.
47
59
 
48
60
  Tested client/tool versions: none recorded. Status: authored; end-to-end execution and vendor connections untested. [Catalog](catalog.json) owns machine-readable status and source references.
49
61
 
@@ -1,6 +1,6 @@
1
1
  # Site and context brief
2
2
 
3
- ID: `site-context-brief` | Version: `1.0.0` | Goal: `build-content` or `prepare-campaign`
3
+ ID: `site-context-brief` | Version: `1.0.1` | Goal: `build-content` or `prepare-campaign`
4
4
 
5
5
  Use this recipe to establish the site, audience, existing assets and evidence before choosing work. Modes: local, hosted, external. Required inputs: the chosen site or product, intended audience, requested outcome and supplied pages or context. Missing inputs become explicit questions in the brief; do not invent them.
6
6
 
@@ -20,6 +20,6 @@ The agent can prepare the brief from supplied material without an AgentLinkOps a
20
20
 
21
21
  If access fails, distinguish missing authentication, missing scope, unavailable feature, quota and temporary failure. Continue from supplied context; record the unresolved operation without retrying indefinitely. If a file exists or changed since the last checkpoint, compare it before proposing a revision and preserve customer edits.
22
22
 
23
- Complete when the brief can support a concrete task with its evidence limits intact. Continue to [sourced asset](sourced-linkable-asset.md) or [campaign handoff](qualified-campaign-handoff.md). No publishing or sending happens in this recipe.
23
+ Complete when the brief can support a concrete task with its evidence limits intact. Use the customer's chosen asset or campaign process for the next task. Packaged continuations are optional and separately selected: choose `agentlinkops agent setup --recipe sourced-linkable-asset` before reading [sourced asset](sourced-linkable-asset.md), or `--recipe qualified-campaign-handoff` before reading [campaign handoff](qualified-campaign-handoff.md). Check selection support as described in [command notes](command-notes.md). No publishing or sending happens in this recipe.
24
24
 
25
25
  Tested client/tool versions: none recorded. Status: authored; end-to-end execution and vendor connections untested. [Catalog](catalog.json) owns machine-readable status and source references.
@@ -1,25 +1,25 @@
1
1
  # Sourced linkable asset
2
2
 
3
- ID: `sourced-linkable-asset` | Version: `1.0.0` | Goal: `build-content`
3
+ ID: `sourced-linkable-asset` | Version: `1.0.1` | Goal: `build-content`
4
4
 
5
5
  Use this recipe for a reference, guide, comparison, dataset, calculator or template that helps a named reader. Modes: local, hosted, external. Required inputs: the site/context brief, reader task, candidate asset or existing page, source material and intended output destination.
6
6
 
7
7
  ## Capabilities and ownership
8
8
 
9
- The agent can write a sourced draft without an AgentLinkOps account. Fetching sources and editing a website depend on the user's available tools. Hosted backlink research is optional and requires actual access. The user's site or content system owns the asset; local CRM storage is optional. For page work, preserve the site's rendering and routing conventions and inspect nearby pages before editing. This recipe contains the writing and review sequence; continue below without loading another asset router.
9
+ The agent can write a sourced draft without an AgentLinkOps account. Fetching sources and editing a website depend on the user's available tools. Hosted backlink research is optional and requires actual access. The user's site or content system owns the asset; local CRM storage is optional. For page work, preserve the site's rendering and routing conventions and inspect nearby pages before editing. Use the customer's chosen source, editorial and review process. Selecting this recipe does not install the optional content skills or require that writing method; see [optional writing and review guidance](command-notes.md#optional-writing-and-review-guidance) if the customer chooses the packaged passes.
10
10
 
11
11
  ## Steps and checkpoints
12
12
 
13
- 1. Read the site's content instructions and inspect existing pages for the same reader task. Define the useful addition before creating another URL. Start with the site's content skill when present, then use the packaged passes below. If a required review resource is unavailable, identify it before marking the asset reviewed.
14
- 2. Write through [source-cited-content-builder](../../skills/source-cited-content-builder/SKILL.md). Preserve source title, publisher, publication date when available, URL and the evidence supporting each factual claim. Mark original calculations and assumptions. Do not manufacture experience or results.
15
- 3. Run [source-cited-humanizer](../../skills/source-cited-humanizer/SKILL.md) as a separate editorial pass, then [content-defingerprinting](../../skills/content-defingerprinting/SKILL.md) on the final authored text. Save the actual checks and findings.
16
- 4. Apply [internal-linking-optimizer](../../skills/internal-linking-optimizer/SKILL.md) to the chosen format. For owned pages, verify destinations and inbound discovery at release. A document or email handoff needs relevant links without webpage quotas. Use the site's image workflow when the asset calls for images.
17
- 5. Run [pre-publish-review](../../skills/pre-publish-review/SKILL.md). Checkpoint the draft revision, intended URL, source records, review receipts and unresolved findings. Read back the artifact from its chosen destination.
13
+ 1. Read the site's applicable content instructions and inspect existing pages for the same reader task. Define the useful addition before creating another URL. Identify the customer's required writing and review steps. If a required review resource is unavailable, identify it before marking the asset reviewed.
14
+ 2. Write with the customer's chosen source process. Preserve source title, publisher, publication date when available, URL and the evidence supporting each factual claim. Mark original calculations and assumptions. Do not manufacture experience or results.
15
+ 3. Use the customer's editorial and final-text review process. Save the actual checks, reviewer or tool used, revision and unresolved findings. A selected packaged pass keeps its own rules; an unrun pass is not a completed review.
16
+ 4. Check links for the chosen format. For owned pages, verify destinations and inbound discovery at release. A document or email handoff needs relevant links without webpage quotas. Use the site's image workflow when the asset calls for images.
17
+ 5. Complete the customer's required release review. Checkpoint the draft revision, intended URL, source records, review receipts and unresolved findings. Read back the artifact from its chosen destination.
18
18
 
19
19
  ## Recovery and completion
20
20
 
21
21
  Missing sources leave claims unverified; remove or qualify them before review. Missing review tools leave the corresponding pass incomplete. Preserve an existing page and customer edits when resuming. A draft, green build or copied command does not prove publication. If release is authorized separately, verify the actual public content after release and attach that receipt.
22
22
 
23
- Complete this recipe with a reviewed draft and explicit publication state, or an unfinished draft with the precise remaining check. Continue to [campaign handoff](qualified-campaign-handoff.md) only with an honest asset state; a pitch must not describe an unpublished asset as live.
23
+ Complete this recipe with a reviewed draft and explicit publication state, or an unfinished draft with the precise remaining check. A campaign continuation uses the customer's chosen process. If the customer chooses the packaged [campaign handoff](qualified-campaign-handoff.md), select it separately with `agentlinkops agent setup --recipe qualified-campaign-handoff` before reading it. Carry an honest asset state into either process; a pitch must not describe an unpublished asset as live.
24
24
 
25
25
  Tested client/tool versions: none recorded. Status: authored; end-to-end execution and vendor connections untested. [Catalog](catalog.json) owns machine-readable status and source references.
@@ -26,6 +26,7 @@ import json
26
26
  import os
27
27
  from pathlib import Path
28
28
  import sqlite3
29
+ import stat
29
30
  import sys
30
31
  import urllib.error
31
32
  import urllib.parse
@@ -117,6 +118,31 @@ def migrations():
117
118
  return result
118
119
 
119
120
 
121
+ def require_private_file(info):
122
+ if not stat.S_ISREG(info.st_mode):
123
+ raise CRMError("CRM storage must be a regular file", "DATABASE_ERROR")
124
+ if os.name == "posix" and stat.S_IMODE(info.st_mode) != 0o600:
125
+ raise CRMError("CRM storage requires mode 0600; secure existing files or use a filesystem that enforces private permissions", "PRIVATE_STORAGE_REQUIRED")
126
+
127
+
128
+ def open_private_file(path):
129
+ descriptor = os.open(path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
130
+ identity = None
131
+ try:
132
+ identity = os.fstat(descriptor)
133
+ require_private_file(identity)
134
+ return descriptor
135
+ except Exception:
136
+ # Check the created inode is still empty before cleanup.
137
+ try:
138
+ current = os.lstat(path)
139
+ if identity and (current.st_dev, current.st_ino) == (identity.st_dev, identity.st_ino) and current.st_size == 0:
140
+ os.unlink(path)
141
+ finally:
142
+ os.close(descriptor)
143
+ raise
144
+
145
+
120
146
  def connect(path, allow_create=False):
121
147
  path = Path(path).expanduser()
122
148
  if path.is_symlink():
@@ -125,8 +151,8 @@ def connect(path, allow_create=False):
125
151
  raise CRMError("CRM does not exist; run init with a workspace ID", "DATABASE_ERROR")
126
152
  if allow_create and not path.exists():
127
153
  path.parent.mkdir(parents=True, exist_ok=True)
128
- descriptor = os.open(path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
129
- os.close(descriptor)
154
+ os.close(open_private_file(path))
155
+ require_private_file(path.lstat())
130
156
  connection = sqlite3.connect(str(path), timeout=10, isolation_level=None)
131
157
  connection.row_factory = sqlite3.Row
132
158
  connection.execute("PRAGMA foreign_keys = ON")
@@ -172,8 +198,7 @@ def migrate(connection, path, workspace_id=None, initialize=False):
172
198
  backup = None
173
199
  if version and pending:
174
200
  backup = str(Path(path).expanduser()) + ".pre-v" + str(version + 1) + "." + dt.datetime.now().strftime("%Y%m%d%H%M%S%f") + ".bak"
175
- descriptor = os.open(backup, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
176
- os.close(descriptor)
201
+ os.close(open_private_file(backup))
177
202
  with sqlite3.connect(backup) as target:
178
203
  connection.backup(target)
179
204
  connection.execute("BEGIN IMMEDIATE")
@@ -500,7 +525,7 @@ def write_new(path, content, suffix=None):
500
525
  path = Path(path).expanduser()
501
526
  if suffix and path.suffix.lower() != suffix:
502
527
  raise CRMError(f"Output file must end in {suffix}")
503
- descriptor = os.open(path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
528
+ descriptor = open_private_file(path)
504
529
  with os.fdopen(descriptor, "w", encoding="utf-8", newline="") as output:
505
530
  output.write(content)
506
531
 
@@ -0,0 +1,121 @@
1
+ #!/usr/bin/env python3
2
+ """Save/read back an explicitly mapped draft in a customer-selected local Outreach CRM.
3
+ No network, send, sequence activation, or contact/authority-state writes.
4
+ """
5
+ import argparse
6
+ import hashlib
7
+ import importlib.util
8
+ import json
9
+ from pathlib import Path
10
+ import re
11
+ from urllib.parse import urlsplit
12
+
13
+ spec = importlib.util.spec_from_file_location('portable_handoff', Path(__file__).with_name('portable-handoff.py'))
14
+ portable = importlib.util.module_from_spec(spec)
15
+ spec.loader.exec_module(portable)
16
+
17
+
18
+ def text(value, label):
19
+ if not isinstance(value, str) or not value.strip():
20
+ raise ValueError(label + ' must be explicit')
21
+ return value
22
+
23
+
24
+ def host(value):
25
+ parsed = urlsplit(text(value, 'site'))
26
+ if parsed.scheme not in ('https', 'http') or not parsed.hostname or parsed.username or parsed.password:
27
+ raise ValueError('site must be an explicit URL without credentials')
28
+ return parsed.hostname.lower()
29
+
30
+
31
+ def prepare(root, request, body):
32
+ fields = {'version', 'crm_project', 'workspace_id', 'project_id', 'site', 'operation_id',
33
+ 'original_request', 'local_id', 'external_id', 'observation', 'reply',
34
+ 'suppression', 'subject', 'body_sha256'}
35
+ if not isinstance(request, dict) or set(request) != fields or type(request['version']) is not int or request['version'] != 1:
36
+ raise ValueError('explicit version 1 draft request required; see local CRM handoff reference')
37
+ for field in fields - {'version', 'original_request'}:
38
+ text(request[field], field)
39
+ project = request['crm_project']
40
+ if not re.fullmatch(r'[a-z0-9][a-z0-9_-]*', project):
41
+ raise ValueError('crm_project must be one project directory name')
42
+ root = Path(root).resolve(strict=True)
43
+ project_dir = root / 'projects' / project
44
+ # Refuse symlink redirection in destination directories, including an existing
45
+ # operation directory. This is a cooperating local writer, not an OS sandbox.
46
+ for path in (root / 'projects', project_dir, project_dir / 'config.json', project_dir / 'drafts'):
47
+ if path.is_symlink():
48
+ raise ValueError('destination symlink refused: ' + str(path))
49
+ config = json.loads((project_dir / 'config.json').read_text(encoding='utf-8'))
50
+ if not isinstance(config, dict) or config.get('project') != project or host(config.get('site')) != host(request['site']):
51
+ raise ValueError('CRM config identity/site mismatch; preserve original mapping')
52
+ if not (project_dir / 'drafts').is_dir():
53
+ raise ValueError('selected CRM drafts directory missing; initialize that project first')
54
+ original = request['original_request']
55
+ if not isinstance(original, dict) or set(original) != {'destination', 'workspace_id', 'project_id', 'command', 'arguments', 'idempotency_key'}:
56
+ raise ValueError('complete original non-secret request tuple required')
57
+ for field in ('destination', 'workspace_id', 'project_id', 'command', 'idempotency_key'):
58
+ text(original[field], 'original_request.' + field)
59
+ if not isinstance(original['arguments'], dict):
60
+ raise ValueError('original request arguments must be an object')
61
+ if any(original[field] != request[field] for field in ('workspace_id', 'project_id')):
62
+ raise ValueError('original request scope mismatch; preserve original mapping')
63
+ if request['observation'] not in ('unknown', 'present', 'provisional_loss', 'confirmed_loss'):
64
+ raise ValueError('explicit supported observation required')
65
+ if request['reply'] not in ('unknown', 'none', 'replied') or request['suppression'] not in ('unknown', 'clear', 'suppressed'):
66
+ raise ValueError('explicit reply/suppression observation required')
67
+ if '\n' in request['subject'] or '\r' in request['subject']:
68
+ raise ValueError('subject must be one line')
69
+ body.decode('utf-8')
70
+ if not body or hashlib.sha256(body).hexdigest() != request['body_sha256']:
71
+ raise ValueError('body bytes do not match the original draft hash')
72
+ identity = {field: request[field] for field in ('crm_project', 'workspace_id', 'project_id', 'operation_id', 'local_id', 'external_id')}
73
+ directory = project_dir / 'drafts' / ('agentlinkops-' + portable.digest(identity))
74
+ if directory.is_symlink():
75
+ raise ValueError('destination operation symlink refused')
76
+ manifest = {'version': 1, 'destination': 'local-outreach-crm-drafts', 'request': request,
77
+ 'body_file': 'body.txt', 'send_eligible': False, 'messages_sent': 0,
78
+ 'native_vendor_acceptance': False, 'hosted_scope_verification': 'customer_supplied_unverified'}
79
+ return directory, (portable.canonical(manifest) + '\n').encode('utf-8')
80
+
81
+
82
+ def save(root, request, body, readback=False):
83
+ directory, manifest = prepare(root, request, body)
84
+ artifacts = [(directory / 'body.txt', body), (directory / 'manifest.json', manifest)]
85
+ if readback and not directory.is_dir():
86
+ raise ValueError('draft readback missing; resume the original save')
87
+ if not readback:
88
+ directory.mkdir(exist_ok=True)
89
+ with portable._publication_lock(directory / 'draft-transaction'):
90
+ for path, expected in artifacts:
91
+ if path.is_symlink() or (path.exists() and (not path.is_file() or path.read_bytes() != expected)):
92
+ raise ValueError('saved draft differs; retain original operation and inspect ' + str(path))
93
+ if readback and not path.is_file():
94
+ raise ValueError('draft readback incomplete; resume the original save')
95
+ outcomes = [] if readback else [portable._publish_bytes(path, expected) for path, expected in artifacts]
96
+ if any(path.read_bytes() != expected for path, expected in artifacts):
97
+ raise ValueError('draft readback mismatch; preserve original operation')
98
+ return {'operation_id': request['operation_id'], 'crm_project': request['crm_project'],
99
+ 'local_id': request['local_id'], 'external_id': request['external_id'],
100
+ 'directory': str(directory), 'body_sha256': request['body_sha256'],
101
+ 'manifest_sha256': hashlib.sha256(manifest).hexdigest(),
102
+ 'outcome': 'saved_and_read_back' if 'saved_and_read_back' in outcomes else 'identical_readback',
103
+ 'messages_sent': 0, 'send_eligible': False, 'native_vendor_acceptance': False}
104
+
105
+
106
+ def main():
107
+ parser = argparse.ArgumentParser(description=__doc__)
108
+ parser.add_argument('--crm-root', required=True)
109
+ parser.add_argument('--request', required=True)
110
+ parser.add_argument('--body-file', required=True)
111
+ parser.add_argument('--readback', action='store_true')
112
+ args = parser.parse_args()
113
+ request = json.loads(Path(args.request).read_text(encoding='utf-8'))
114
+ print(portable.canonical(save(args.crm_root, request, Path(args.body_file).read_bytes(), args.readback)))
115
+
116
+
117
+ if __name__ == '__main__':
118
+ try:
119
+ main()
120
+ except (ValueError, OSError, KeyError, TypeError) as error:
121
+ raise SystemExit('local CRM draft refused: ' + str(error))