loopctl-mcp-server 3.3.0 → 3.5.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 +3 -3
- package/index.js +39 -4
- package/lib/intake-sources.js +29 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -531,9 +531,9 @@ The order to wire it up, the stage machine these tools move a story through, and
|
|
|
531
531
|
| `resolve_escalation` | **Move an escalated story off `escalated`, as a human** (`POST /api/v1/stories/:id/stage/resolve`): `to` is `queued` (work it again), `done` or `failed`. The other half of `escalate_story` — without it a parked story stays parked for ever. Requires `LOOPCTL_USER_KEY` on a key no dispatch minted: a session cannot resolve the escalation it raised. Optional: `reason`. |
|
|
532
532
|
| `force_unclaim_story` | **Take a story back from the agent holding it** (`POST /api/v1/stories/:id/force-unclaim`). Resets it to `agent_status: pending` with `assigned_agent_id` cleared AND makes the delivery stage row follow the release back to `queued`. **A delivery story then goes to `escalated`, not back to the queue** (loopctl US-44.4): an operator taking a story back is a human decision, so a release that leaves the stage row at `queued` escalates it over `operator_released` in the same transaction, spending no attempt against the retry ceiling — it is not left in the queue behind the human's back. Put it back to work with `resolve_escalation` `to: queued`, which releases (a no-op by then) AND re-contracts. A story with no delivery stage row is simply left `pending`. A story parked at `claimed` with nobody on it is the residue of a compensation that did not complete, not what a refused dispatch normally leaves: placement releases the claim inline when a runner refuses, and the claim lease releases it unattended once `claimed_until` passes — reach for this to get it back now, or when both of those left it held. Requeues from any stage a claim holds; a stage no claim holds keeps its stage and is rebound to the new epoch; `done` and `failed` are untouched. For an ESCALATED story use `resolve_escalation` instead — that one releases AND re-contracts, so its `queued` really is placeable. Requires an orchestrator-ROLE key: the action is `exact_role: :orchestrator`, so a user or superadmin key is 403'd like any other non-member. Put it in `LOOPCTL_ORCH_KEY` (which is then the key sent, and a global `LOOPCTL_API_KEY` does not displace it) or, with no orchestrator key set, in `LOOPCTL_API_KEY`. The orchestrator key must be linked to a registered agent (400 otherwise) and the tenant must be human-anchored (403 `custody_tier_required` otherwise). Does not touch `verified_status`. Run again on an already-pending story it is still an operator's release: a row stranded behind the story's claim epoch is rebound, an in-flight row is requeued, and a row then at `queued` — including one already sitting there — is escalated over `operator_released`, as on the first run; a row already escalated, or anywhere else at that epoch, is left alone. Every 500 means the whole release rolled back and the story is still claimed: `audit_chain_append_failed` (the escalation's chain entry was refused) is a server-side condition a re-run meets again until an operator acts; `force_unclaim_failed` (the server log names the step) — call it again. A 422 means the release write itself was rejected. No request body. Required: `story_id` (refused locally if it is not a UUID). |
|
|
533
533
|
| `merge_precondition` | **Run the merge gate over a story's real pull request** (`POST /api/v1/stories/:id/merge-precondition`). Both delivery gates run again over the diff that exists, plus custody, the 12-file / 1000-line hard bound, the head-has-not-moved check and the self-deploy exclusion; the story must be at stage `ci`. **Gate A reads what triage persisted, never the caller**: the lens verdicts recorded with the story's most recent triage, or a human's re-queue of a Gate A escalation — `gate_a_inputs` on the answer says which, and `missing` refuses with `gate_a_inputs_missing`. This tool sends no trio. Decisions: `allow` (recorded against the head; the only thing that licenses a merge), `refuse` (the story is escalated before this returns), `already_merged`, `head_moved`, `unevaluated` (503 with `Retry-After`). Requires an orchestrator- or user-ROLE key (`exact_role: [:orchestrator, :user]`, so an agent key is 403'd); `LOOPCTL_ORCH_KEY` is sent when set, else `LOOPCTL_API_KEY`. 403 `custody_tier_required` without a human anchor, 404 for an unknown story, 409 `change_protocol_enrolled` when the story's intake source is in mode `chg` (the PR route is closed; land with `chg submit` — decided before any GitHub read, nothing escalated), 422 when the story is not at `ci`. Required: `story_id` (refused locally if not a UUID) and `claim_epoch` (a non-negative integer, refused locally otherwise); optional `effect_proof`. |
|
|
534
|
-
| `intake_source_enroll` | **Bind a GitHub repository to a work project and mint its webhook secret** (`POST /api/v1/intake/sources`) — the step that gives the delivery loop an input, and the row `place_dispatch` reads `repo` and `base_branch` from (its 409 `no_intake_source` means this has not been done; 409 `ambiguous_intake_source` means it has been done twice). **The secret is returned ONCE and can never be read again** — the column is encrypted at rest and redacted on the schema, so no other tool carries it and losing it costs a revoke, a re-enrolment and a reconfigured GitHub webhook. **This tool does not return it either:** a tool result lands in the transcript and the audit log, so it is written to the required `secret_file` with mode 0600 (the handling `runner_enroll` gives a runner credential) and the result carries the source row, the `webhook_url` and that path. The path is reserved before the request, so an existing file or an unwritable directory is refused with nothing enrolled, and any outcome that is not a clean creation withholds the response body — an unparsed 2xx body IS the secret. A 2xx carrying the source and the secret but no webhook path is RECOVERED rather than discarded (the path is derived from the source id, flagged `webhook_url_derived`), and an outcome that proves a source id but no usable secret REVOKES that source rather than leaving it holding the repository's unique slot. Then configure GitHub: Payload URL = the returned `webhook_url`, content type `application/json`, Secret = the contents of the file, and the **Issues** event only — `gh api repos/OWNER/REPO/hooks -f name=web -f config[url]=<webhook_url> -f config[content_type]=json -f config[secret]="$(cat <secret_file>)" -f 'events[]=issues'`. Every later delivery failure is the SAME 401 `invalid_signature` — unknown or revoked source, suspended tenant, missing or wrong signature, or a payload whose `repository.full_name` does not match — so the webhook's Recent Deliveries tab cannot tell you which. `base_branch` is set HERE: it defaults to `master` when the body does not name it, so name `main` for a repository created on GitHub since 2020 or every dispatch is cut from a trunk that does not exist. It is not nullable (a dispatch must name a branch), so null or blank is refused rather than falling back to the default, and `intake_source_update` changes it afterwards. Refusals: 403 `api_key_mint_forbidden` if your key was minted by a dispatch (this mints a credential belonging to no lineage, so only an unlineaged operator key may), 403 `custody_tier_required` on an agent-rooted tenant, 422 for a repository that is not `owner/name`, an ACTIVE source already binding it, a project that is missing / archived / not a work project, or a `target_epic_id` outside that project. `mode` is the merge route: `pr` (the default when omitted; the merge gate reads a pull request) or `chg` (Epic 47: the change protocol lands the repository's changes with `chg submit`, and `merge_precondition` refuses every story on the source with 409 `change_protocol_enrolled`); any other value, null included, is refused. A `chg` enrolment needs `change_events_secret` (or, preferred, `change_events_secret_file`, a path the tool reads so the value never enters the transcript) AND `chg_root_dispatch_id` in the same call — refused locally and 422 at the server without them — and a root that is not a parentless, unrevoked, orchestrator-role dispatch of this tenant is 422 `chg_root_invalid` (a child, a revoked or agent-role dispatch, another tenant's and an unknown id all alike). The change-events secret is write-only: no response and no tool result ever carries it. `required_checks` are the CI checks story verification judges a story's commit by, GitHub Actions job names, bounded in number and length by the server; `local-gate` is refused because whoever pushed posts it. Story verification judges a story's commit against the same `required_checks` in EITHER mode, satisfied only by a job of a push run of the story's branch; it first needs a GitHub credential for this tenant, its own token (`set_github_credential`) or an entry in the operator's `VERIFICATION_OPERATOR_TOKEN_TENANTS` (`credential_unavailable` otherwise, whatever the list holds); then a `pr` source that names none records `no_required_checks` and reads nothing, so naming them is how a pr source opts in, and a source that names some has its (tenant, repository) credential resolved before any GitHub read (`credential_unavailable` if there is none). Required: `repo_full_name`, `project_id`, `secret_file`. Optional: `base_branch`, `mode`, `required_checks`, `change_events_secret` / `change_events_secret_file`, `chg_root_dispatch_id`, and `target_epic_id` — **omitting the epic is not a neutral default**: with no epic every triaged report is escalated to a human and every record stays `pending_triage` until `intake_source_update` names one. Requires `LOOPCTL_USER_KEY`. |
|
|
535
|
-
| `intake_source_list` | List the tenant's intake sources (`GET /api/v1/intake/sources`): `id` (the webhook URL is `/api/v1/intake/github/<id>`), `project_id`, `repo_full_name`, `base_branch`, `mode` (`pr` or `chg`), `chg_root_dispatch_id`, `target_epic_id`, `revoked_at`, timestamps. **The webhook secret is not here and is not anywhere**, and neither is a `chg` source's change-events secret — it is returned once by `intake_source_enroll`, so this is not how to recover one. Use it to find the source behind a 409 `no_intake_source` or `ambiguous_intake_source`, to check a repository's `base_branch` before placing work, and to identify a source left behind by an enrolment whose outcome was unknown. Optional: `include_revoked`. Requires `LOOPCTL_USER_KEY`; the only one of the four that does not also need a human-anchored tenant. |
|
|
536
|
-
| `intake_source_update` | **Set where a source's work lands** (`PATCH /api/v1/intake/sources/:id`): `target_epic_id`, the epic triaged stories are created in, `base_branch`, the branch every dispatch for this repository is cut from, `mode` (`pr` or `chg`), a `chg` source's `change_events_secret` / `change_events_secret_file` and `chg_root_dispatch_id`, and `required_checks`, the checks story verification judges a story's commit by — once the tenant has a GitHub credential, its own token (`set_github_credential`) or an entry in the operator's `VERIFICATION_OPERATOR_TOKEN_TENANTS` (`credential_unavailable` otherwise), a source naming none records `no_required_checks` and reads nothing, and one naming some has its (tenant, repository) credential resolved before any GitHub read (`local-gate` is refused). Presence decides — a field you do not name is left exactly as it was, and naming none of them is 422 `nothing_to_update`. `mode` has no cleared state either, and a null or unknown value is refused locally. Each implement dispatch records the base branch it was placed under, and the merge gate reads that; `mode` is not bound that way — the gate reads the source's current mode. **Switching to `chg` closes the PR route for every story on the source at once** (409 `change_protocol_enrolled`; land with `chg submit`), and the switch itself is 409 `pr_route_in_flight`, naming `story_ids`, while a story of the project sits at `ci` holding a merge allow it has not merged on. The resulting source must carry both credentials, named here or kept from an earlier `chg` stint (422 naming the missing one), and the root is re-checked on every write that leaves the source `chg`, a secret rotation or a base-branch edit included (422 `chg_root_invalid`; switching to `pr` is never blocked by it). Switching to `pr` KEEPS both and ignores them; on a `pr` source an explicit null clears either, on a `chg` source it is a 422. Naming either credential, or setting `mode` to `chg` (which re-activates the kept ones), needs the tenant's operator key, one no dispatch minted (403 `chg_registration_forbidden` otherwise — a key minted by a dispatch beneath yours is refused the same way), and the secret never comes back. **Do not send `target_epic_id: null` to mean "not changing this":** an explicit null is the only way to CLEAR the epic, which returns the source to escalating every report instead of filing a story. `base_branch` has no cleared state and a null there is refused locally. This is how a source already pointed at the wrong trunk is corrected — `intake_source_enroll` takes `base_branch` itself, so a `main` repository no longer needs a second call — and the remedy for a source with no epic — every record from it stays `pending_triage` and is retried until one is named, then they promote on the next run with nothing lost. Revoked sources are 404, not a no-op: revoking clears the target so the epic can be deleted, and repointing one would put that block back. 422 when the epic is not in this source's project. Required: `source_id` (refused locally if it is not a UUID — the server answers the same 404 for malformed and unknown). Requires `LOOPCTL_USER_KEY` and a human-anchored tenant. |
|
|
534
|
+
| `intake_source_enroll` | **Bind a GitHub repository to a work project and mint its webhook secret** (`POST /api/v1/intake/sources`) — the step that gives the delivery loop an input, and the row `place_dispatch` reads `repo` and `base_branch` from (its 409 `no_intake_source` means this has not been done; 409 `ambiguous_intake_source` means it has been done twice). **The secret is returned ONCE and can never be read again** — the column is encrypted at rest and redacted on the schema, so no other tool carries it and losing it costs a revoke, a re-enrolment and a reconfigured GitHub webhook. **This tool does not return it either:** a tool result lands in the transcript and the audit log, so it is written to the required `secret_file` with mode 0600 (the handling `runner_enroll` gives a runner credential) and the result carries the source row, the `webhook_url` and that path. The path is reserved before the request, so an existing file or an unwritable directory is refused with nothing enrolled, and any outcome that is not a clean creation withholds the response body — an unparsed 2xx body IS the secret. A 2xx carrying the source and the secret but no webhook path is RECOVERED rather than discarded (the path is derived from the source id, flagged `webhook_url_derived`), and an outcome that proves a source id but no usable secret REVOKES that source rather than leaving it holding the repository's unique slot. Then configure GitHub: Payload URL = the returned `webhook_url`, content type `application/json`, Secret = the contents of the file, and the **Issues** event only — `gh api repos/OWNER/REPO/hooks -f name=web -f config[url]=<webhook_url> -f config[content_type]=json -f config[secret]="$(cat <secret_file>)" -f 'events[]=issues'`. Every later delivery failure is the SAME 401 `invalid_signature` — unknown or revoked source, suspended tenant, missing or wrong signature, or a payload whose `repository.full_name` does not match — so the webhook's Recent Deliveries tab cannot tell you which. `base_branch` is set HERE: it defaults to `master` when the body does not name it, so name `main` for a repository created on GitHub since 2020 or every dispatch is cut from a trunk that does not exist. It is not nullable (a dispatch must name a branch), so null or blank is refused rather than falling back to the default, and `intake_source_update` changes it afterwards. Refusals: 403 `api_key_mint_forbidden` if your key was minted by a dispatch (this mints a credential belonging to no lineage, so only an unlineaged operator key may), 403 `custody_tier_required` on an agent-rooted tenant, 422 for a repository that is not `owner/name`, an ACTIVE source already binding it, a project that is missing / archived / not a work project, or a `target_epic_id` outside that project. `mode` is the merge route: `pr` (the default when omitted; the merge gate reads a pull request) or `chg` (Epic 47: the change protocol lands the repository's changes with `chg submit`, and `merge_precondition` refuses every story on the source with 409 `change_protocol_enrolled`); any other value, null included, is refused. A `chg` enrolment needs `change_events_secret` (or, preferred, `change_events_secret_file`, a path the tool reads so the value never enters the transcript) AND `chg_root_dispatch_id` in the same call — refused locally and 422 at the server without them — and a root that is not a parentless, unrevoked, orchestrator-role dispatch of this tenant is 422 `chg_root_invalid` (a child, a revoked or agent-role dispatch, another tenant's and an unknown id all alike). The change-events secret is write-only: no response and no tool result ever carries it. `required_checks` are the CI checks story verification judges a story's commit by, GitHub Actions job names, bounded in number and length by the server; `local-gate` is refused because whoever pushed posts it. Story verification judges a story's commit against the same `required_checks` in EITHER mode, satisfied only by a job of a push run of the story's branch; it first needs a GitHub credential for this tenant, its own token (`set_github_credential`) or an entry in the operator's `VERIFICATION_OPERATOR_TOKEN_TENANTS` (`credential_unavailable` otherwise, whatever the list holds); then a `pr` source that names none records `no_required_checks` and reads nothing, so naming them is how a pr source opts in, and a source that names some has its (tenant, repository) credential resolved before any GitHub read (`credential_unavailable` if there is none). Required: `repo_full_name`, `project_id`, `secret_file`. `triage_requires_ticket` decides which issues are triaged at all: false (the default), every issue the webhook delivers; true, only an issue HomeCareBilling filed from a support ticket (its single `#HCB-xxxxxxxx` Ticket line confirmed by the HomeCareBilling footer, which intake records as `ticket_id`), and every other issue stays `detected` and costs nothing; anything but a boolean is refused. It filters on the format, it does not authenticate: anyone who can open an issue can type it. Optional: `base_branch`, `mode`, `required_checks`, `triage_requires_ticket`, `change_events_secret` / `change_events_secret_file`, `chg_root_dispatch_id`, and `target_epic_id` — **omitting the epic is not a neutral default**: with no epic every triaged report is escalated to a human and every record stays `pending_triage` until `intake_source_update` names one. Requires `LOOPCTL_USER_KEY`. |
|
|
535
|
+
| `intake_source_list` | List the tenant's intake sources (`GET /api/v1/intake/sources`): `id` (the webhook URL is `/api/v1/intake/github/<id>`), `project_id`, `repo_full_name`, `base_branch`, `mode` (`pr` or `chg`), `required_checks`, `triage_requires_ticket`, `chg_root_dispatch_id`, `target_epic_id`, `revoked_at`, timestamps. **The webhook secret is not here and is not anywhere**, and neither is a `chg` source's change-events secret — it is returned once by `intake_source_enroll`, so this is not how to recover one. Use it to find the source behind a 409 `no_intake_source` or `ambiguous_intake_source`, to check a repository's `base_branch` before placing work, and to identify a source left behind by an enrolment whose outcome was unknown. Optional: `include_revoked`. Requires `LOOPCTL_USER_KEY`; the only one of the four that does not also need a human-anchored tenant. |
|
|
536
|
+
| `intake_source_update` | **Set where a source's work lands** (`PATCH /api/v1/intake/sources/:id`): `target_epic_id`, the epic triaged stories are created in, `base_branch`, the branch every dispatch for this repository is cut from, `mode` (`pr` or `chg`), a `chg` source's `change_events_secret` / `change_events_secret_file` and `chg_root_dispatch_id`, and `required_checks`, the checks story verification judges a story's commit by — once the tenant has a GitHub credential, its own token (`set_github_credential`) or an entry in the operator's `VERIFICATION_OPERATOR_TOKEN_TENANTS` (`credential_unavailable` otherwise), a source naming none records `no_required_checks` and reads nothing, and one naming some has its (tenant, repository) credential resolved before any GitHub read (`local-gate` is refused), and `triage_requires_ticket`, which issues are triaged at all (true: only issues HomeCareBilling filed from a support ticket; read at dispatch time, so it reaches stories already waiting at `detected`; null is refused). Presence decides — a field you do not name is left exactly as it was, and naming none of them is 422 `nothing_to_update`. `mode` has no cleared state either, and a null or unknown value is refused locally. Each implement dispatch records the base branch it was placed under, and the merge gate reads that; `mode` is not bound that way — the gate reads the source's current mode. **Switching to `chg` closes the PR route for every story on the source at once** (409 `change_protocol_enrolled`; land with `chg submit`), and the switch itself is 409 `pr_route_in_flight`, naming `story_ids`, while a story of the project sits at `ci` holding a merge allow it has not merged on. The resulting source must carry both credentials, named here or kept from an earlier `chg` stint (422 naming the missing one), and the root is re-checked on every write that leaves the source `chg`, a secret rotation or a base-branch edit included (422 `chg_root_invalid`; switching to `pr` is never blocked by it). Switching to `pr` KEEPS both and ignores them; on a `pr` source an explicit null clears either, on a `chg` source it is a 422. Naming either credential, or setting `mode` to `chg` (which re-activates the kept ones), needs the tenant's operator key, one no dispatch minted (403 `chg_registration_forbidden` otherwise — a key minted by a dispatch beneath yours is refused the same way), and the secret never comes back. **Do not send `target_epic_id: null` to mean "not changing this":** an explicit null is the only way to CLEAR the epic, which returns the source to escalating every report instead of filing a story. `base_branch` has no cleared state and a null there is refused locally. This is how a source already pointed at the wrong trunk is corrected — `intake_source_enroll` takes `base_branch` itself, so a `main` repository no longer needs a second call — and the remedy for a source with no epic — every record from it stays `pending_triage` and is retried until one is named, then they promote on the next run with nothing lost. Revoked sources are 404, not a no-op: revoking clears the target so the epic can be deleted, and repointing one would put that block back. 422 when the epic is not in this source's project. Required: `source_id` (refused locally if it is not a UUID — the server answers the same 404 for malformed and unknown). Requires `LOOPCTL_USER_KEY` and a human-anchored tenant. |
|
|
537
537
|
| `intake_source_revoke` | **Revoke an intake source** (`DELETE /api/v1/intake/sources/:id`). The verb is DELETE and the action is a revoke: `revoked_at` is stamped, the row is kept, and `intake_source_list` still returns it with `include_revoked`; nothing already received is discarded. Afterwards every delivery to that URL is refused 401 `invalid_signature` exactly as a wrong secret is, so the GitHub webhook does not know it has been cut off and should be deleted on the repository too. Idempotent — a second revoke answers 200 with the original `revoked_at`. It frees the repository's uniqueness slot (the index is partial on not-yet-revoked), so a source with a lost secret, a wrong project or a typo'd repository is corrected by revoking and enrolling again; and it clears `target_epic_id`, which is what makes that epic deletable and is the remedy an epic-delete refusal names. Required: `source_id` (refused locally if it is not a UUID). Requires `LOOPCTL_USER_KEY` and a human-anchored tenant. |
|
|
538
538
|
|
|
539
539
|
### Dispatch & Chain of Custody (v2) Tools
|
package/index.js
CHANGED
|
@@ -4278,7 +4278,10 @@ const TOOLS = [
|
|
|
4278
4278
|
"REFUSED for stories that have any loopctl dispatch lineage — non-pending agent_status, assigned_agent_id, implementer_dispatch_id, " +
|
|
4279
4279
|
"or verifier_dispatch_id set. Also refused for stories already `:verified` (idempotent no-op when the same payload is sent) or `:rejected`. " +
|
|
4280
4280
|
"Records a provenance marker in `metadata.backfill` plus an audit event and a `story.backfilled` webhook. " +
|
|
4281
|
-
"REQUIRES `reason`. Strongly recommend passing `evidence_url` (http/https, no credentials in userinfo) and `pr_number`."
|
|
4281
|
+
"REQUIRES `reason`. Strongly recommend passing `evidence_url` (http/https, no credentials in userinfo) and `pr_number`. " +
|
|
4282
|
+
"A story bound to a chg change takes verify_story's chg gates too: 409 review_missing_escalated, " +
|
|
4283
|
+
"chg_review_not_for_landed_revision, chg_landing_not_trunk_verified or chg_landing_reverted, and 503 " +
|
|
4284
|
+
"chg_trunk_unevaluated with Retry-After (loopctl US-47.6).",
|
|
4282
4285
|
inputSchema: {
|
|
4283
4286
|
type: "object",
|
|
4284
4287
|
properties: {
|
|
@@ -4978,7 +4981,15 @@ const TOOLS = [
|
|
|
4978
4981
|
"applied landed event of its current change), else 409 " +
|
|
4979
4982
|
"chg_review_not_for_landed_revision — have chg dispatch a review naming that sha; and " +
|
|
4980
4983
|
"never while its stage is escalated over review_missing, else 409 " +
|
|
4981
|
-
"review_missing_escalated — a human resolves it first (resolve_escalation)."
|
|
4984
|
+
"review_missing_escalated — a human resolves it first (resolve_escalation). " +
|
|
4985
|
+
"Its CI evidence is chg's trunk verifier (loopctl US-47.6): the trunk commit of that " +
|
|
4986
|
+
"landing (data.merge_sha) must be an ancestor of the repo's verified/master, else 409 " +
|
|
4987
|
+
"chg_landing_not_trunk_verified (retry once chg has verified it), and no reverted event " +
|
|
4988
|
+
"may follow the landing, else 409 chg_landing_reverted. A forge read that times out or " +
|
|
4989
|
+
"fails, a missing verified/master ref, or no forge credential for the repo is 503 " +
|
|
4990
|
+
"chg_trunk_unevaluated with Retry-After (error.reason names which) — nothing was " +
|
|
4991
|
+
"decided; retry after that many seconds. The forge credential (the tenant's own GitHub " +
|
|
4992
|
+
"token) needs read access to the repo's contents.",
|
|
4982
4993
|
inputSchema: {
|
|
4983
4994
|
type: "object",
|
|
4984
4995
|
properties: {
|
|
@@ -5035,7 +5046,8 @@ const TOOLS = [
|
|
|
5035
5046
|
"ADMIN USE ONLY: backfill pre-existing completed work into pending stories that never entered the dispatch lifecycle. " +
|
|
5036
5047
|
"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
5048
|
"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
|
|
5049
|
+
"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, " +
|
|
5050
|
+
"or with chg_landing_not_trunk_verified, chg_landing_reverted or chg_trunk_unevaluated, the same trunk-evidence gate (loopctl US-47.6).",
|
|
5039
5051
|
inputSchema: {
|
|
5040
5052
|
type: "object",
|
|
5041
5053
|
properties: {
|
|
@@ -5073,7 +5085,9 @@ const TOOLS = [
|
|
|
5073
5085
|
"Refused 409 chg_root_cannot_verify for the whole epic, verifying nothing, when the key " +
|
|
5074
5086
|
"descends from an intake source's chg_root_dispatch_id (loopctl US-47.5). A story bound " +
|
|
5075
5087
|
"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
|
|
5088
|
+
"chg_review_not_for_landed_revision, the same landed-review gate verify_story applies, " +
|
|
5089
|
+
"or with chg_landing_not_trunk_verified, chg_landing_reverted or chg_trunk_unevaluated " +
|
|
5090
|
+
"(the trunk-evidence gate verify_story applies, loopctl US-47.6).",
|
|
5077
5091
|
inputSchema: {
|
|
5078
5092
|
type: "object",
|
|
5079
5093
|
properties: {
|
|
@@ -8974,6 +8988,17 @@ const TOOLS = [
|
|
|
8974
8988
|
"read (`credential_unavailable` if there is none). Distinct, non-blank names, bounded in number and length by the server (422 past them). " +
|
|
8975
8989
|
"`local-gate` is refused: whoever pushed posts it, so it is only recorded.",
|
|
8976
8990
|
},
|
|
8991
|
+
triage_requires_ticket: {
|
|
8992
|
+
type: "boolean",
|
|
8993
|
+
description:
|
|
8994
|
+
"Optional, false when omitted. WHICH ISSUES ARE TRIAGED. False: every issue the " +
|
|
8995
|
+
"webhook delivers. True: only an issue HomeCareBilling filed from a support " +
|
|
8996
|
+
"ticket (its single `#HCB-xxxxxxxx` Ticket line confirmed by the HomeCareBilling " +
|
|
8997
|
+
"footer, recorded as `ticket_id`); every other issue stays `detected` and costs " +
|
|
8998
|
+
"nothing. A format filter, not authentication. Anything but a boolean is refused " +
|
|
8999
|
+
"(422). Changed " +
|
|
9000
|
+
"later with intake_source_update.",
|
|
9001
|
+
},
|
|
8977
9002
|
secret_file: {
|
|
8978
9003
|
type: "string",
|
|
8979
9004
|
description:
|
|
@@ -9127,6 +9152,16 @@ const TOOLS = [
|
|
|
9127
9152
|
"read (`credential_unavailable` if there is none). Distinct, non-blank names, bounded in number and length by the server (422 past them). " +
|
|
9128
9153
|
"`local-gate` is refused. Omit to leave the current list alone.",
|
|
9129
9154
|
},
|
|
9155
|
+
triage_requires_ticket: {
|
|
9156
|
+
type: "boolean",
|
|
9157
|
+
description:
|
|
9158
|
+
"WHICH ISSUES ARE TRIAGED. True: only an issue HomeCareBilling filed from a " +
|
|
9159
|
+
"support ticket (its single `#HCB-xxxxxxxx` Ticket line confirmed by the " +
|
|
9160
|
+
"HomeCareBilling footer, recorded as `ticket_id`) is sent to triage; a format " +
|
|
9161
|
+
"filter, not authentication. Every other issue stays `detected` and costs " +
|
|
9162
|
+
"nothing. False: every issue is triaged. Read at dispatch time, so it reaches the " +
|
|
9163
|
+
"stories already waiting. Null is refused; omit the field to leave it alone.",
|
|
9164
|
+
},
|
|
9130
9165
|
},
|
|
9131
9166
|
required: ["source_id"],
|
|
9132
9167
|
},
|
package/lib/intake-sources.js
CHANGED
|
@@ -326,6 +326,19 @@ export function requiredChecksRefusal(checks) {
|
|
|
326
326
|
return null;
|
|
327
327
|
}
|
|
328
328
|
|
|
329
|
+
/**
|
|
330
|
+
* `triage_requires_ticket`: a boolean, never null (`Source.validate_triage_requires_ticket/1`
|
|
331
|
+
* answers 422 for null). True triages only issues HomeCareBilling filed from a support ticket.
|
|
332
|
+
*/
|
|
333
|
+
export function triageRequiresTicketRefusal(value) {
|
|
334
|
+
if (typeof value === "boolean") return null;
|
|
335
|
+
|
|
336
|
+
return refuse(
|
|
337
|
+
"`triage_requires_ticket` must be true (triage only issues filed from a support ticket) " +
|
|
338
|
+
"or false (triage every issue). It cannot be null; leave it out to keep the current value.",
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
|
|
329
342
|
export function sourcePath(sourceId) {
|
|
330
343
|
return `${SOURCES_PATH}/${encodeURIComponent(sourceId)}`;
|
|
331
344
|
}
|
|
@@ -356,6 +369,7 @@ export function publicSource(source) {
|
|
|
356
369
|
base_branch: source.base_branch,
|
|
357
370
|
mode: source.mode,
|
|
358
371
|
required_checks: source.required_checks,
|
|
372
|
+
triage_requires_ticket: source.triage_requires_ticket,
|
|
359
373
|
// `change_events_secret` is NEVER named here: write-only (US-47.2).
|
|
360
374
|
chg_root_dispatch_id: source.chg_root_dispatch_id,
|
|
361
375
|
target_epic_id: source.target_epic_id,
|
|
@@ -391,6 +405,7 @@ export async function enrollIntakeSource(
|
|
|
391
405
|
base_branch,
|
|
392
406
|
mode,
|
|
393
407
|
required_checks,
|
|
408
|
+
triage_requires_ticket,
|
|
394
409
|
secret_file,
|
|
395
410
|
change_events_secret,
|
|
396
411
|
change_events_secret_file,
|
|
@@ -449,6 +464,11 @@ export async function enrollIntakeSource(
|
|
|
449
464
|
if (badChecks) return badChecks;
|
|
450
465
|
}
|
|
451
466
|
|
|
467
|
+
if (triage_requires_ticket !== undefined) {
|
|
468
|
+
const badFlag = triageRequiresTicketRefusal(triage_requires_ticket);
|
|
469
|
+
if (badFlag) return badFlag;
|
|
470
|
+
}
|
|
471
|
+
|
|
452
472
|
// The `chg` credentials (US-47.2), resolved and judged before a secret file is reserved.
|
|
453
473
|
const chg = await chgCredentials(
|
|
454
474
|
{ change_events_secret, change_events_secret_file, chg_root_dispatch_id },
|
|
@@ -510,6 +530,7 @@ export async function enrollIntakeSource(
|
|
|
510
530
|
if (base_branch !== undefined) body.base_branch = base_branch;
|
|
511
531
|
if (mode !== undefined) body.mode = mode;
|
|
512
532
|
if (required_checks !== undefined) body.required_checks = required_checks;
|
|
533
|
+
if (triage_requires_ticket !== undefined) body.triage_requires_ticket = triage_requires_ticket;
|
|
513
534
|
Object.assign(body, chg.fields);
|
|
514
535
|
|
|
515
536
|
let result;
|
|
@@ -707,6 +728,12 @@ export async function updateIntakeSource(
|
|
|
707
728
|
body.required_checks = args.required_checks;
|
|
708
729
|
}
|
|
709
730
|
|
|
731
|
+
if (args.triage_requires_ticket !== undefined) {
|
|
732
|
+
const badFlag = triageRequiresTicketRefusal(args.triage_requires_ticket);
|
|
733
|
+
if (badFlag) return badFlag;
|
|
734
|
+
body.triage_requires_ticket = args.triage_requires_ticket;
|
|
735
|
+
}
|
|
736
|
+
|
|
710
737
|
// Switching to `chg` may rely on credentials an earlier `chg` stint left on the row, so their
|
|
711
738
|
// absence here is the SERVER's to judge (422 naming the missing one); only their shape is
|
|
712
739
|
// judged here.
|
|
@@ -717,7 +744,8 @@ export async function updateIntakeSource(
|
|
|
717
744
|
if (Object.keys(body).length === 0) {
|
|
718
745
|
return refuse(
|
|
719
746
|
"Nothing to update. Name at least one of `target_epic_id` (null clears it), " +
|
|
720
|
-
"`base_branch`, `mode`, `required_checks`, `
|
|
747
|
+
"`base_branch`, `mode`, `required_checks`, `triage_requires_ticket`, " +
|
|
748
|
+
"`change_events_secret` (or " +
|
|
721
749
|
"`change_events_secret_file`) or `chg_root_dispatch_id`. A body carrying none of them " +
|
|
722
750
|
"is refused 422 `nothing_to_update`, because a field you do not send is left exactly " +
|
|
723
751
|
"as it was.",
|