loopctl-mcp-server 3.2.0 → 3.3.0

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.
package/README.md CHANGED
@@ -259,7 +259,7 @@ Epic 39 Repo Coordination Bus — a lightweight, tenant-isolated channel for age
259
259
  | Tool | Description |
260
260
  |---|---|
261
261
  | `contract_story` | Agent acknowledges a story's acceptance criteria. Transitions pending -> contracted. Refused 409 `story_held` when the story's delivery stage is `escalated`, `done` or `failed` — it is not available to agents even when it reads `pending`; an escalated story becomes available again only after `resolve_escalation` sends it to `queued`. |
262
- | `claim_story` | Agent claims a contracted story with pessimistic locking. Transitions contracted -> assigned. On a loopctl with claim leases the result leads with the claim's `claim_epoch` and `claimed_until`; keep the epoch for `renew_story_claim`. Both are absent on an older server. Refused 409 `story_held` when the story's delivery stage is `escalated`, `done` or `failed` — even when it reads `pending` or `contracted`; an escalated story is claimable again only after `resolve_escalation` sends it to `queued`, a done or failed one never is. |
262
+ | `claim_story` | Agent claims a contracted story with pessimistic locking. Transitions contracted -> assigned. On a loopctl with claim leases the result leads with the claim's `claim_epoch` and `claimed_until`; keep the epoch for `renew_story_claim`. Both are absent on an older server. Refused 409 `story_held` when the story's delivery stage is `escalated`, `done` or `failed` — even when it reads `pending` or `contracted`; an escalated story is claimable again only after `resolve_escalation` sends it to `queued`, a done or failed one never is. Refused 409 `chg_root_review_only` when the key descends from an intake source's `chg_root_dispatch_id`: chg-server's keys may only review. |
263
263
  | `renew_story_claim` | Renew your claim's lease (`POST /stories/:id/renew-claim`, agent key). A claim not renewed before `claimed_until` (default 24 hours, `STORY_CLAIM_LEASE_SECONDS`) is released back to `pending` under you, so renew well inside it on any story held longer. A driver-placed claim — one a placement took for a runner dispatch — is capped at its dispatch deadline (`claim_lease_cap`: placed_at + `wall_clock_seconds` + `DISPATCH_LEASE_GRACE_SECONDS` at the claim, sent to the runner as `deadline_at`; moved forward to a resume's time or the runner's acceptance + `wall_clock_seconds` + the grace, only while the claim is live; never earlier than a `deadline_at` already sent), and its `claimed_until` already IS that cap: renewing it writes nothing and returns the claim as it stands, never moving `claimed_until` past it. Required: `story_id`, `claim_epoch` (from the claim). Refusals pass through: 400 epoch missing or malformed, 422 `not_claimed`, 409 `stale_claim_epoch` (the claim has ended: stop working it), 409 `not_claimant`, 409 `lease_cap_reached` (a driver-placed claim already at its cap: nothing is renewed, stop working it). Needs a loopctl server with claim leases. |
264
264
  | `escalate_story` | Park a story you hold for a HUMAN and stop (`POST /stories/:id/escalate`, agent key). This is what an unattended session calls INSTEAD of asking a question — a headless run has no way to ask, so escalating is the affordance. Use it when the request contradicts an existing story, KB decision or documented behaviour, when it inverts or removes behaviour a previous story deliberately added, when it is a workflow change rather than a defect fix, or when it needs a business call — not for an ordinary blocker. ONE-WAY from your side: only a human moves the story off `escalated`, so on success STOP and report. Required: `story_id`, `claim_epoch`, `reason` (verbatim, treated as untrusted data, length capped by the server in codepoints — a 400 names the bound); optional `payload` object. Idempotent under the same epoch. Refusals pass through: 409 `stale_claim_epoch`, 409 `not_claimant`, 404 `unknown_story_stage`, 400 for a malformed epoch or reason. An orchestrator or user key is 403'd on purpose. |
265
265
  | `start_story` | Agent starts work on a claimed story. Transitions assigned -> implementing. |
@@ -269,22 +269,22 @@ Epic 39 Repo Coordination Bus — a lightweight, tenant-isolated channel for age
269
269
 
270
270
  | Tool | Description |
271
271
  |---|---|
272
- | `report_story` | Reviewer confirms the implementation is done. Transitions implementing -> reported_done. Accepts optional `token_usage` object. Under the LCP-1 §9.3 signed profile, pass the `claim` from `custody_sign_claim`. |
273
- | `review_complete` | Record that a review has been completed for a story. Required before verify. Under the signed profile, pass the `claim` from `custody_sign_claim`. |
272
+ | `report_story` | Reviewer confirms the implementation is done. Transitions implementing -> reported_done. Accepts optional `token_usage` object. Under the LCP-1 §9.3 signed profile, pass the `claim` from `custody_sign_claim`. Refused 409 `chg_root_review_only` for a key descending from an intake source's `chg_root_dispatch_id`. |
273
+ | `review_complete` | Record that a review has been completed for a story. Required before verify. Under the signed profile, pass the `claim` from `custody_sign_claim`. On a story bound to a chg change (loopctl US-47.5), after the existing custody gates: only a key chg-server minted for the review — a strict descendant of the intake source's `chg_root_dispatch_id` — may record it (409 `chg_reviewer_required`, the root key included); `commit_sha` is REQUIRED and must be the sha of a submitted revision of the story's current change (422 `commit_sha_not_bound`); one reviewer dispatch records one verdict (409 `chg_review_already_recorded`). `commit_sha` is ignored on any other story. |
274
274
 
275
275
  ### Verification Tools (orchestrator key)
276
276
 
277
277
  | Tool | Description |
278
278
  |---|---|
279
- | `verify_story` | Orchestrator verifies a reported_done story. Transitions reported_done -> verified. Under the signed profile, pass the `claim` from `custody_sign_claim`. |
280
- | `reject_story` | Orchestrator rejects a story with a reason. The reject releases the claim, and a delivery story may be re-contracted or escalated in the same transaction. A 500 `audit_chain_append_failed` (the escalation's chain entry was refused) means the WHOLE call rolled back: the story is unchanged, and a re-run meets the same server-side condition until an operator acts. |
279
+ | `verify_story` | Orchestrator verifies a reported_done story. Transitions reported_done -> verified. Under the signed profile, pass the `claim` from `custody_sign_claim`. Refused 409 `chg_root_cannot_verify` for a key descending from an intake source's `chg_root_dispatch_id` (loopctl US-47.5). |
280
+ | `reject_story` | Orchestrator rejects a story with a reason. The reject releases the claim, and a delivery story may be re-contracted or escalated in the same transaction. A 500 `audit_chain_append_failed` (the escalation's chain entry was refused) means the WHOLE call rolled back: the story is unchanged, and a re-run meets the same server-side condition until an operator acts. Refused 409 `chg_root_cannot_verify` for a key descending from an intake source's `chg_root_dispatch_id` (loopctl US-47.5). |
281
281
 
282
282
  ### Bulk Tools (orchestrator key)
283
283
 
284
284
  | Tool | Description |
285
285
  |---|---|
286
- | `bulk_mark_complete` | **Backfill-only.** Bulk-marks pre-existing, never-dispatched stories (pending, unassigned, no dispatch lineage) as complete in a single call. Dispatched stories are refused — they must go through the normal report → review → verify flow. |
287
- | `verify_all_in_epic` | Bulk verify all reported_done, unverified stories in an epic. |
286
+ | `bulk_mark_complete` | **Backfill-only.** Bulk-marks pre-existing, never-dispatched stories (pending, unassigned, no dispatch lineage) as complete in a single call. Dispatched stories are refused — they must go through the normal report → review → verify flow. Refused 409 `chg_root_cannot_verify` for the whole batch for a chg-rooted key (loopctl US-47.5). |
287
+ | `verify_all_in_epic` | Bulk verify all reported_done, unverified stories in an epic. Refused 409 `chg_root_cannot_verify` for the whole epic for a chg-rooted key (loopctl US-47.5). |
288
288
 
289
289
  ### Token Efficiency Tools
290
290
 
@@ -664,6 +664,8 @@ loopctl enforces that nobody marks their own work as done. The API returns `409`
664
664
 
665
665
  The implementer's final action is `request_review`. All subsequent steps (report, review, verify) must come from different agents.
666
666
 
667
+ On a story bound to a chg change (loopctl US-47.5) the review comes from chg-server: `review_complete` takes it only from a key chg-server minted beneath its registered `chg_root_dispatch_id` (409 `chg_reviewer_required`), naming the reviewed revision as `commit_sha` (422 `commit_sha_not_bound`), one verdict per reviewer dispatch (409 `chg_review_already_recorded`). chg-server's keys never certify or implement: `verify_story`, `reject_story`, `verify_all_in_epic` and `bulk_mark_complete` answer 409 `chg_root_cannot_verify`, `claim_story` and `report_story` 409 `chg_root_review_only`. A chg story that lands with no review for its landed revision is escalated over `review_missing` and is not verified without a human.
668
+
667
669
  ## Witness protocol (STH)
668
670
 
669
671
  Every authenticated request echoes the caller's last-known Signed Tree Head (STH)
package/index.js CHANGED
@@ -1372,12 +1372,15 @@ async function reportStory({ story_id, artifact_type, artifact_path, token_usage
1372
1372
  return toContent(result);
1373
1373
  }
1374
1374
 
1375
- async function reviewComplete({ story_id, review_type, findings_count, fixes_count, disproved_count, summary, claim }) {
1375
+ async function reviewComplete({ story_id, review_type, findings_count, fixes_count, disproved_count, summary, commit_sha, claim }) {
1376
1376
  const body = { review_type };
1377
1377
  if (findings_count != null) body.findings_count = findings_count;
1378
1378
  if (fixes_count != null) body.fixes_count = fixes_count;
1379
1379
  if (disproved_count != null) body.disproved_count = disproved_count;
1380
1380
  if (summary) body.summary = summary;
1381
+ // loopctl US-47.5: the reviewed chg revision. Forwarded as given and judged server-side
1382
+ // (422 commit_sha_not_bound), so a malformed value reaches the one place that can say why.
1383
+ if (commit_sha != null) body.commit_sha = commit_sha;
1381
1384
  if (claim) body.claim = claim;
1382
1385
 
1383
1386
  const result = await apiCall(
@@ -1400,7 +1403,7 @@ async function verifyStory({ story_id, summary, review_type, claim }) {
1400
1403
  if (review_type) body.review_type = review_type;
1401
1404
  if (claim) body.claim = claim;
1402
1405
 
1403
- // `exact_role: :orchestrator` (`story_verification_controller.ex:29-30`). When
1406
+ // `exact_role: :orchestrator` (`story_verification_controller.ex:30-31`). When
1404
1407
  // LOOPCTL_ORCH_KEY is set it is sent EXACTLY, because `resolveKey` would otherwise
1405
1408
  // discard it in favour of a global LOOPCTL_API_KEY on the very calls it was set for.
1406
1409
  // When it is NOT set, the global key still goes — see lib/custody-key.js for why the
@@ -1418,7 +1421,7 @@ async function verifyStory({ story_id, summary, review_type, claim }) {
1418
1421
  }
1419
1422
 
1420
1423
  async function rejectStory({ story_id, reason }) {
1421
- // `exact_role: :orchestrator` (`story_verification_controller.ex:29-30`) — see
1424
+ // `exact_role: :orchestrator` (`story_verification_controller.ex:30-31`) — see
1422
1425
  // lib/custody-key.js.
1423
1426
  const orch = orchestratorKeyArgs();
1424
1427
 
@@ -1436,7 +1439,7 @@ async function rejectStory({ story_id, reason }) {
1436
1439
 
1437
1440
  async function bulkMarkComplete({ stories }) {
1438
1441
  // stories: [{story_id, summary, review_type}]
1439
- // `exact_role: :orchestrator` (`bulk_operations_controller.ex:24-25`) — see
1442
+ // `exact_role: :orchestrator` (`bulk_operations_controller.ex:25-26`) — see
1440
1443
  // lib/custody-key.js.
1441
1444
  const orch = orchestratorKeyArgs();
1442
1445
 
@@ -1451,7 +1454,7 @@ async function bulkMarkComplete({ stories }) {
1451
1454
  }
1452
1455
 
1453
1456
  async function verifyAllInEpic({ epic_id, review_type, summary }) {
1454
- // `exact_role: :orchestrator` (`story_verification_controller.ex:29-30`, `:verify_all`) —
1457
+ // `exact_role: :orchestrator` (`story_verification_controller.ex:30-31`, `:verify_all`) —
1455
1458
  // see lib/custody-key.js.
1456
1459
  const orch = orchestratorKeyArgs();
1457
1460
 
@@ -3454,7 +3457,7 @@ async function resolveEscalation(args) {
3454
3457
  }
3455
3458
 
3456
3459
  // #846: force-unclaim is `exact_role: :orchestrator`
3457
- // (`story_verification_controller.ex:29-30`), so this is the one delivery-loop verb that wants
3460
+ // (`story_verification_controller.ex:30-31`), so this is the one delivery-loop verb that wants
3458
3461
  // the ORCH key. Same selection as the other four custody verbs: when LOOPCTL_ORCH_KEY is set it
3459
3462
  // is sent EXACTLY, because `resolveKey` (`index.js:132-138`) would otherwise discard it for a
3460
3463
  // global LOOPCTL_API_KEY on the one call it was set for; when it is not set, a global key still
@@ -4725,7 +4728,9 @@ const TOOLS = [
4725
4728
  "reads pending or contracted — move on. An escalated story is claimable again only " +
4726
4729
  "after resolve_escalation sends it to queued; a done or failed one never is. Refused " +
4727
4730
  "409 dependencies_not_met when a story it depends on, or one in an epic its epic " +
4728
- "depends on, is not verified: list_blocked_stories names them.",
4731
+ "depends on, is not verified: list_blocked_stories names them. Refused 409 " +
4732
+ "chg_root_review_only when the key descends from an intake source's " +
4733
+ "chg_root_dispatch_id: chg-server's keys may only review.",
4729
4734
  inputSchema: {
4730
4735
  type: "object",
4731
4736
  properties: {
@@ -4871,7 +4876,9 @@ const TOOLS = [
4871
4876
  description:
4872
4877
  "Reviewer (a DIFFERENT agent from the implementer) confirms the implementation is done. " +
4873
4878
  "Chain-of-custody: the implementing agent cannot call this. " +
4874
- "Transitions implementing -> reported_done. Uses the ORCH key.",
4879
+ "Transitions implementing -> reported_done. Uses the ORCH key. Refused 409 " +
4880
+ "chg_root_review_only when the key descends from an intake source's " +
4881
+ "chg_root_dispatch_id: chg-server's keys may only review.",
4875
4882
  inputSchema: {
4876
4883
  type: "object",
4877
4884
  properties: {
@@ -4906,7 +4913,16 @@ const TOOLS = [
4906
4913
  name: "review_complete",
4907
4914
  description:
4908
4915
  "Record that a review has been completed for a story. " +
4909
- "Must be called before verify_story. Uses the ORCH key.",
4916
+ "Must be called before verify_story. Uses the ORCH key. " +
4917
+ "On a story BOUND TO A chg CHANGE (loopctl US-47.5) three more rules apply, after the " +
4918
+ "existing custody gates (self_review_blocked, caller_lineage_required, " +
4919
+ "unresolvable_dispatch_lineage), never instead of them: the key must be one chg-server " +
4920
+ "minted for the review — a strict descendant of the intake source's " +
4921
+ "chg_root_dispatch_id — else 409 chg_reviewer_required (chg-server's root key is " +
4922
+ "refused too); commit_sha is REQUIRED and must be the sha of a submitted revision of " +
4923
+ "the story's current change, else 422 commit_sha_not_bound; and one reviewer dispatch " +
4924
+ "records one verdict, a second is 409 chg_review_already_recorded. On any other story " +
4925
+ "commit_sha is ignored and not stored.",
4910
4926
  inputSchema: {
4911
4927
  type: "object",
4912
4928
  properties: {
@@ -4935,6 +4951,13 @@ const TOOLS = [
4935
4951
  type: "string",
4936
4952
  description: "Optional: summary of the review outcome.",
4937
4953
  },
4954
+ commit_sha: {
4955
+ type: "string",
4956
+ description:
4957
+ "REQUIRED on a story bound to a chg change: the reviewed revision commit, 40 or " +
4958
+ "64 lowercase hex characters, the sha of a submitted revision of the story's " +
4959
+ "current change (422 commit_sha_not_bound otherwise). Ignored on any other story.",
4960
+ },
4938
4961
  claim: CLAIM_SCHEMA,
4939
4962
  },
4940
4963
  required: ["story_id", "review_type"],
@@ -4947,7 +4970,15 @@ const TOOLS = [
4947
4970
  description:
4948
4971
  "Orchestrator verifies a reported_done story. " +
4949
4972
  "Requires a review_record to exist (call review_complete first). " +
4950
- "Transitions reported_done -> verified. Uses the ORCH key.",
4973
+ "Transitions reported_done -> verified. Uses the ORCH key. Refused 409 " +
4974
+ "chg_root_cannot_verify when the key descends from an intake source's " +
4975
+ "chg_root_dispatch_id (loopctl US-47.5): chg-server's keys review and never verify — " +
4976
+ "use an orchestrator key outside chg's tree. A story BOUND TO A chg CHANGE is verified " +
4977
+ "only on a chg review of its LANDED revision (the revision commit of the newest " +
4978
+ "applied landed event of its current change), else 409 " +
4979
+ "chg_review_not_for_landed_revision — have chg dispatch a review naming that sha; and " +
4980
+ "never while its stage is escalated over review_missing, else 409 " +
4981
+ "review_missing_escalated — a human resolves it first (resolve_escalation).",
4951
4982
  inputSchema: {
4952
4983
  type: "object",
4953
4984
  properties: {
@@ -4976,7 +5007,10 @@ const TOOLS = [
4976
5007
  "The reject releases the claim, and a delivery story may be re-contracted or escalated " +
4977
5008
  "in the same transaction. A 500 `audit_chain_append_failed` (the escalation's chain " +
4978
5009
  "entry was refused) means the WHOLE call rolled back: the story is unchanged, and a " +
4979
- "re-run meets the same server-side condition until an operator acts.",
5010
+ "re-run meets the same server-side condition until an operator acts. Refused 409 " +
5011
+ "chg_root_cannot_verify when the key descends from an intake source's " +
5012
+ "chg_root_dispatch_id (loopctl US-47.5): reject is a custody decision chg-server's " +
5013
+ "keys never make.",
4980
5014
  inputSchema: {
4981
5015
  type: "object",
4982
5016
  properties: {
@@ -4999,7 +5033,9 @@ const TOOLS = [
4999
5033
  description:
5000
5034
  "Bulk mark multiple stories as complete (backfill-only for never-dispatched work). " +
5001
5035
  "ADMIN USE ONLY: backfill pre-existing completed work into pending stories that never entered the dispatch lifecycle. " +
5002
- "For dispatched stories, use the normal report/review/verify flow. Each story entry needs a story_id, summary, and review_type. Uses the ORCH key.",
5036
+ "For dispatched stories, use the normal report/review/verify flow. Each story entry needs a story_id, summary, and review_type. Uses the ORCH key. " +
5037
+ "Refused 409 chg_root_cannot_verify for the whole batch when the key descends from an intake source's chg_root_dispatch_id (loopctl US-47.5). " +
5038
+ "A story bound to a chg change is a per-story error leading with review_missing_escalated or chg_review_not_for_landed_revision, the same landed-review gate verify_story applies.",
5003
5039
  inputSchema: {
5004
5040
  type: "object",
5005
5041
  properties: {
@@ -5033,7 +5069,11 @@ const TOOLS = [
5033
5069
  name: "verify_all_in_epic",
5034
5070
  description:
5035
5071
  "Bulk verify all reported_done, unverified stories in an epic. " +
5036
- "Convenience endpoint for the orchestrator after a review pass. Uses the ORCH key.",
5072
+ "Convenience endpoint for the orchestrator after a review pass. Uses the ORCH key. " +
5073
+ "Refused 409 chg_root_cannot_verify for the whole epic, verifying nothing, when the key " +
5074
+ "descends from an intake source's chg_root_dispatch_id (loopctl US-47.5). A story bound " +
5075
+ "to a chg change is skipped into errors with review_missing_escalated or " +
5076
+ "chg_review_not_for_landed_revision, the same landed-review gate verify_story applies.",
5037
5077
  inputSchema: {
5038
5078
  type: "object",
5039
5079
  properties: {
@@ -2,9 +2,9 @@
2
2
  * Key selection for the `exact_role: :orchestrator` custody verbs (loopctl #846 review round 2).
3
3
  *
4
4
  * WHICH VERBS. `verify`, `reject` and `force_unclaim` plus the aggregate `verify_all`
5
- * (`lib/loopctl_web/controllers/story_verification_controller.ex:29-30`), and bulk
5
+ * (`lib/loopctl_web/controllers/story_verification_controller.ex:30-31`), and bulk
6
6
  * `verify`/`reject`/`mark_complete`
7
- * (`lib/loopctl_web/controllers/bulk_operations_controller.ex:24-25`). All are mounted
7
+ * (`lib/loopctl_web/controllers/bulk_operations_controller.ex:25-26`). All are mounted
8
8
  * `exact_role: :orchestrator`, where the role hierarchy does NOT apply: `RequireRole`'s
9
9
  * exact-role clause tests `api_key.role == exact_role` (`require_role.ex:65-75`), so a `:user`
10
10
  * or `:superadmin` key is refused there exactly as an `:agent` key is.
@@ -21,7 +21,7 @@
21
21
  * - `resolve_escalation` is the HUMAN half of the escalation pair: a `:user` key no dispatch
22
22
  * minted. The agent key that raises an escalation is 403'd on it by design.
23
23
  * - `force_unclaim_story` is gated `exact_role: :orchestrator`
24
- * (`story_verification_controller.ex:29-30`), so it needs an ORCHESTRATOR-ROLE key and a
24
+ * (`story_verification_controller.ex:30-31`), so it needs an ORCHESTRATOR-ROLE key and a
25
25
  * higher-privileged one does NOT substitute: `RequireRole`'s exact-role clause tests
26
26
  * `api_key.role == exact_role` (`require_role.ex:65-75`), so a `:user` or `:superadmin`
27
27
  * key is 403'd there exactly as an agent key is. That is a chain-of-custody gate, not an
@@ -293,7 +293,7 @@ export async function resolveEscalation({ story_id, to, reason } = {}, { userKey
293
293
  * No request body — the story is named in the path.
294
294
  *
295
295
  * ORCHESTRATOR ROLE, and exactly that: the action is `exact_role: :orchestrator`
296
- * (`story_verification_controller.ex:29-30`), so a user or superadmin key is refused —
296
+ * (`story_verification_controller.ex:30-31`), so a user or superadmin key is refused —
297
297
  * `RequireRole`'s exact-role clause tests `api_key.role == exact_role`
298
298
  * (`require_role.ex:65-75`).
299
299
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loopctl-mcp-server",
3
- "version": "3.2.0",
3
+ "version": "3.3.0",
4
4
  "description": "MCP server for loopctl — structural trust for AI development loops",
5
5
  "type": "module",
6
6
  "main": "index.js",