submit-suggestion · diff
git:20261023.0918e06 to git:20261026.ba5add7
11 added, 0 removed. Audit A to A.
---
name: submit-suggestion
description: Propose declarative configuration updates securely by committing file changes and submitting pull requests (merge requests on GitLab) for SRE review, including a request to delete a resource, which its "Deleting a resource" section answers with a pull request that removes the declaring manifest, or a stated stop when the repository does not declare it. Not for fleet-audit finding fixes — the fleet-audit skill opens and tracks those PRs itself.
---
# submit-suggestion - Secure GitOps Pull Request Orchestrator
This skill equips the Platform Agent to propose declarative file updates, GKE infrastructure adjustments, or configuration changes securely by committing local repository changes and submitting pull requests (PRs; merge requests on GitLab) for human review. The repository is named as `owner/name` on github.com, `<host>/owner/name` on a GitHub Enterprise Server, or `<host>/<path>` — `gitlab.com/<group>/<subgroup>/<project>` — for one on another forge, exactly as it appears in the managed repository list.
## When to Use
- **Declarative File Provisioning:** Triggered when new GKE manifests or configs are requested.
- **Configuration Upgrades:** Triggered when upgrading version configurations, security patches, or network policies.
- **Governance Policy Syncs:** Triggered when compliance playbooks or settings require updates.
- **Any change in the GitOps repository** reaches review through this skill, but a Terraform, Config Connector or Helm-chart change is written by its authoring skill first (Step 2's table).
- **Deletions:** Triggered when someone asks to delete a resource. See [Deleting a resource](#deleting-a-resource) before Step 1: a declared object is removed in a pull request, anything else is a stated stop.
_Crucially, you are strictly forbidden from executing direct, manual mutations. All changes must flow through a secure PR path — this skill, or the **fleet-audit** skill for fixes of its findings (below)._
## When NOT to Use
- **Answering a request that asked for a diagnosis, not a change.** "Investigate
and report the root cause", "why is this crashlooping", "what would you
change" are questions; their answer is the finding and the proposed manifest
in the reply, marked as a recommendation. Open the pull request only when the
request asks for one, or for the change to be submitted or fixed; `SOUL.md`
§3, item 3 draws the line, and the unattended case is `fleet-audit`'s own
promotion, which `finish` does on a scheduled run, never this skill. A pull
request nobody asked for is a write on their repository they now have to
triage.
- **Fixing a fleet-audit finding.** The bullets above match audit fixes too — a
security patch, a policy update — which is exactly why this warning exists. If
the change addresses a fleet-audit finding (it carries a finding id, or an
`[audit]` ledger issue lists that exact deviation as a finding), the
**fleet-audit** skill opens that pull request itself, through its `remediate`
subcommand — see `/opt/data/profiles/platform/skills/fleet-audit/references/remediation.md` for the invocation. That path
keys the branch on the files the fix touches, so a rerun cannot open a
duplicate: a live PR is left untouched rather than force-pushed over, and one
the harness withdrew as stale is re-proposed on the same branch. It applies
the audit labels, links the ledger, and closes the PR when the finding stops
reproducing. A PR opened through _this_ skill gets none of that — nothing
dedupes it and nothing ever closes it, which is how one workload's findings
once became five near-duplicate PRs.
Two cautions when you take that path. `remediate` consumes the findings
document the audit run wrote (`start` prints its path); if it no longer
exists, stop and say the fix should be requested as `/remediate <finding-id>`
on the ledger — do **not** run `start` yourself to mint a fresh document (it
scrubs that stream's workspace, possibly under a scheduled run), and never
hand-write one. And a change a user asked for on its own terms is not an
audit fix, even when the same file appears in a ledger — this section is
about fixes _of findings_, not about files findings happen to mention.
## Execution Instructions
Follow these steps to make, commit, and submit your GitOps suggestions asynchronously:
### Step 1: Prepare
Never run the plain `git` on this machine and never work from wherever your
shell happens to start. That `git` is a different program: it reaches the
network with a credential, and it is not the one your working copy answers to.
`prepare` brings the repository down and stands you on the branch this change
goes on.
The script path is spelled out from `$HERMES_HOME` rather than as `./skills/…`
because this skill is reached from a kanban card as well as from a cron turn,
and a card dispatch starts you in the task's workspace, not the profile
directory. `$HERMES_HOME` is the profile directory in both. Use that form
everywhere below, including for `vcs.py` in Step 5.
If you do meet a `No such file or directory` on one of these scripts, do **not**
recover by writing the absolute path out: `/opt/data/profiles/platform/…` is
refused by the gateway lifecycle guard, under an error about restarting the
gateway that has nothing to do with what you ran. Observed live — the refusal
sent one worker on to report a change it had not made.
```bash
python3 "$HERMES_HOME"/skills/submit-suggestion/scripts/submit_suggestion.py prepare \
--repo "<owner>/<repo>" \
--branch "platform-agent/<change_type>-<target_id>"
```
_(Example: `--repo "acme/fleet" --branch "platform-agent/provision-mercury-09"` or `--repo "acme/fleet" --branch "platform-agent/upgrade-policy-baseline"`)_
In a multi-repository environment, pass `--repo "<owner>/<repo>"` for the repository your task targets (identified from cluster annotations or task context per SOUL.md §3, item 6).
Pass `--repo` as the repository registration names it: `<owner>/<repo>` on
GitHub, or `<host>/<owner>/<repo>` on another declared forge (such as GitLab or
Gitea). `prepare` echoes the canonical repository name in `repo`; pass that
`repo` value to every later `--repo` in this skill and in `version-control`
calls so installs that serve more than one forge do not have to guess which
host a bare slug belongs to.
It prints one JSON line. **Keep it — Step 2 works inside its `workspace`.** The
`workspace` is named for your branch as well as the repository, because
`/opt/data/scratch` is shared with every other card: another card suggesting a
change to the same repository right now derives a different branch name for it,
so it works in a different directory. Different, not protected — every card here
runs as the same account and the whole tree is readable and writable from all of
them. What keeps two suggestions apart is that no two of them share a branch
name, which is why the name has to describe the change.
```json
{
"workspace": "/opt/data/scratch/vcs/github__acme__fleet__platform-agent__provision-mercury-09",
"repo": "acme/fleet",
"branch": "platform-agent/provision-mercury-09",
"base": "main",
"started_from": "main",
"proposal": ""
}
```
`base` is what the change merges into: the branch the operator configured for
this repository when there is one (the broker refuses a pull request onto any
other), else the repository's own default branch, not a hardcoded `main` — or,
when a pull request for this branch is already open, whatever that one is
already targeting. It comes from the broker, never from your environment:
exporting `GITOPS_BASE_BRANCH` or any other variable does not change it.
If `prepare` is refused with `BASE_BRANCH_MISSING`, the configured base named
in the message does not exist on the repository. That is for an operator to
fix; report it and stop rather than naming another branch.
`started_from` and `proposal` are two halves of one answer — whether this is new
work or another round on a change already under review. `prepare` asks the forge
rather than taking your word for it. When a pull request is open for the branch,
its URL is in `proposal`, the copy is taken **of that branch** so every reviewed
revision comes down with it, and `started_from` is the branch itself. Otherwise
`proposal` is empty, the copy is taken of the base, and `started_from` is the
base.
There is one working copy per repository **and branch**, so preparing a second
change to the same repository does not disturb the first. If the copy for this
branch holds revisions that were never published, `prepare` refuses to replace
it. Finishing them is the way past, not the flag: go to the copy the refusal
names, publish what is in it and open the proposal, and this `prepare` becomes a
second round on that branch instead of a replacement. `--force` deletes those
revisions; it is the answer only once you have read them and decided they should
not exist.
A name can also be spent: the proposal it was last used for is closed or
squash-merged, and its revisions are not in the history you just cloned. If the
forge still holds that branch, a change cut fresh from the base does not build
on it and `submit` would be refused as `BRANCH_DIVERGED`. `prepare` handles this
itself: it asks the forge whether the branch is still there and, when it is,
deletes it — the broker allows that only for a branch under `platform-agent/`,
with no open proposal, whose tip is exactly what a closed proposal from this
repository carried, and every proposal on it opened by this install's
credential. A credential that cannot name its own login cannot show the last
part, so for it the prefix and the same-repository rule are the whole bar.
The log line says which happened. Nothing is lost: on GitHub and on GitLab the
revisions stay reachable from the closed proposal.
If the broker refuses the delete, `prepare` refuses the name and names the
code. `NOT_SPENT` means the branch moved on after its proposal closed;
`BRANCH_MOVED` means something pushed to it a moment ago — run `prepare` once
more, which reads it again; if a person or a sibling card on the same name added
to it, that second run refuses it as `NOT_SPENT`. `OPEN_PROPOSAL` means a
proposal was opened on the name since `prepare` looked; run `prepare` once
more, and it adds to that proposal's branch. `BRANCH_NOT_OURS` means a proposal from it was
not this install's, or the name is not under `platform-agent/`, or it has
carried a full page of proposals, too long a history to read, or an open
proposal targets it (the message says which). Either way, do not delete it another way. `DELETE_REFUSED` means the
repository itself refuses to delete the branch (a branch rule, a hook, or a
credential without the right), and it will refuse again. Choose a different name where
the derived one is only a default, or report the refusal and stop where the name
is fixed. `FORGE_CALL_FAILED`, `GIT_FAILED`, or a delete refused with no code
is different: the delete did not complete (it may have landed before the
failure), and running `prepare` once more, which reads the branch afresh, is
the move; a second failure is reported, not retried. `FORGE_RATE_LIMITED` or
`FORGE_UNAVAILABLE` from the delete is the forge turning it away for now: wait
a few minutes, then run `prepare` once more. The same codes from the read of
the branch that comes before the delete get the same moves, and `prepare` says
which one. A proxy older than the
sandbox cannot read the branch at all; `prepare` then refuses the name with
`BROKER_ROUTE_UNSUPPORTED` and says so — use another name, or report it where the name is fixed.
### Step 2: Make the Changes
**First, hand the edit to the skill that writes its format.** `prepare` is
often the first time you see what the change is written in, and this skill
is the transport, not the author. Before you edit, look at the file or
directory the change lands in:
| The change is | Load and follow first |
| ------------------------------------------------------------------------------------------------------------------------------------ | ------------------------ |
| a Terraform file (`.tf`, `.tf.json`, `.tfvars`), wherever it sits | **gcp-terraform** |
| a Config Connector object (`apiVersion: *.cnrm.cloud.google.com/*`) | **gcp-config-connector** |
| a Helm chart's declaration (a Helm-sourced `RootSync`, a Flux `HelmRelease`, an Argo CD Helm `Application`, kustomize `helmCharts:`) | **gitops-helm-chart** |
Load it with `skill_view` and follow it from its Step 1, its own `prepare`
and checks included (the CRD check for Config Connector, the reconciler form
for a chart, the owning files and pins for Terraform, then `tofu fmt`). You
have edited nothing yet, so nothing is lost: its `prepare` opens the copy
under its own branch name, and its last step submits that branch through
Step 3 here. Leave the copy you prepared here unused. A plain Kubernetes
manifest has no authoring skill; write it here.
**When the branch is already decided, stay on it.** In a review round
(Step 5), or when `prepare` returned a non-empty `proposal`, the revisions
under review are on this copy's branch. Take the authoring skill's reads,
writing rules and checks, but not its Step 1 `prepare`, its branch name or
its submit: edit in this `workspace` and submit from Step 3 here, onto the
same branch. Starting from its Step 1 would open a second pull request.
+ **Implement only the chosen option.** When the request comes from a multi-option
+ triage or audit report (or a single-fix triage report), implement only the
+ option the caller chose (the one marked Recommended when none was named, the
+ single proposed fix, or the `Option <X>` the user selected) and edit only the
+ file(s) that option calls for; if the repository holds both a GitOps sync source
+ path and cluster bootstrap or provisioning copies of the workload, check the
+ repository's GitOps sync declaration for the namespace or workload and edit only
+ the synced workload manifest(s). Never bundle unchosen options
+ or optional hardening into the same pull request, and never touch unrelated
+ manifests or infrastructure definitions.
+
Generate or edit the files **inside the returned `workspace`**.
`git` here holds no credential and cannot reach a forge; it works on the
working copy and nothing else, and `submit` is what sends the result.
```bash
cd <workspace>
# create or edit the declarative files here
git add <file_path_1> <file_path_2>
git commit -m "<conventional_commit_message>"
```
**CRITICAL SECURITY RULE:** explicitly stage only the targeted declarative files
you generated or modified. **Never use `git add .` or `git add -A`** — this is a
real clone on a filesystem you also scratch in, and a blanket add sweeps
transient debugging output, logs and anything else that landed there into a
public pull request.
_(Example: `git add config/manifest.yaml && git commit -m "feat(fleet): provision GKE operator for mercury-09"`)_
Committing here is optional. Uncommitted changes **to files the copy already
tracks** are recorded as a single revision under the `--title` you pass when you
run Step 3, which is what a single-purpose change wants. Commit yourself when
the change deserves more than one revision, or a message that is not the pull
request's headline.
The rule above still holds at Step 3: a file the copy has never seen is not
swept in for you. `submit` refuses and names it, because it cannot tell a
manifest you generated from a log you left behind. Stage the ones that belong
(`git add <path>`) and delete the rest.
### Deleting a resource
A request to delete something (a namespace, a Deployment, a ConfigMap) reaches
review the same way as any other change, and the only delete a pull request
can carry is the removal of a manifest the repository already has. Never add a
file, an annotation or a label to say "delete": nothing reads one you invent,
and a manifest you add declares its object, so merging it keeps the object.
1. **The repository first.** Before Step 1:
```bash
python3 "$HERMES_HOME"/scripts/gitops_preflight.py
```
Any line other than `REGISTERED: ...` is the answer: block with that line as
the reason (`kanban_block(kind="needs_input")`), change nothing, and do not
look for another way to delete it.
2. **Find the declaration.** After Step 1's `prepare`, search the `workspace`
for the object's name, then read each file found and keep the one whose
`kind`, `metadata.name` and, for a namespaced object, `metadata.namespace`
match. A namespace is also declared without a Namespace manifest: by
objects placed in it, a `kustomization.yaml` whose `namespace:` names it, a
Flux `HelmRelease` or `Kustomization` whose `targetNamespace:` names it, a
`namespaces/<name>/` directory (Config Sync's hierarchical layout), or an
Argo CD `Application` with `CreateNamespace=true` deploying into it. Any of
those counts as declared. The search matches the key anywhere on the line,
so JSON and flow-style manifests are found too. Search only the target
cluster's directory (`clusters/<cluster>/`, or the path its reconciler
syncs): the same name under another cluster's directory is another
cluster's object. When the repository has no per-cluster layout and the
name is declared in more than one place, or you cannot tell which
directory is the target's, stop and name each file; never remove a
declaration you have not tied to the target cluster:
```bash
grep -rlE "[\"' {,]?(name|namespace|targetNamespace)[\"']?: *[\"']?<name>[\"']?([ ,}#]|$)" <workspace>/clusters/<cluster>
find <workspace>/clusters/<cluster> -type d -path "*/namespaces/<name>"
```
3. **The repository declares it.** A delete needs the user's confirmation
first (`SOUL.md`); with it in the request, go ahead. Remove only what
declares the object, read document by document, never by file name or by
a `grep` hit alone:
- A file whose every document is the object (for a namespace: the
Namespace or an object in it) is `git rm`'d, and its line comes out of
the directory's `kustomization.yaml` `resources:` list.
- A file that also holds other documents keeps them: delete only the
matching documents from it.
- For a namespace, everything step 2 found goes in the same pull request,
each named in its body, because any one left behind recreates it: the
objects in it, its directory, and the `kustomization.yaml` that lists
only them.
- **The listing above it is an edit.** A removed file or directory that a
parent `kustomization.yaml` names in its `resources:` (for example
`- gitops-owned-scratch/` in `clusters/<cluster>/namespaces/kustomization.yaml`)
loses that one line in the same pull request; left in, the parent no
longer renders and the whole synced path stops applying. Read the parent
directory's `kustomization.yaml` `resources:` list itself (an entry may
be `<name>/`, `./<name>`, `<name>/<file>.yaml` or part of a flow list),
remove only the entries that name what this pull request removes, and
name the edit in the body.
- **A shared declaration is a stop.** A `kustomization.yaml`, Argo CD
`Application`, Flux `Kustomization` or Flux `HelmRelease` that also
creates objects outside the request, or a `namespaces/<name>/` directory with namespaces under it
(Config Sync's hierarchical abstract namespace), would delete those too.
Stop with: "`<file>` also declares `<other objects>`; removing it would
delete them as well. I have changed nothing." Never remove an
`Application` or a `HelmRelease`: an Application's resources finalizer
deletes everything it deploys, and helm-controller uninstalls a removed
release, cluster-scoped objects included. A request that would need one
removed is a stop that names it.
Then continue with Step 3. A delete's pull request body ends with what
step 5 read about the reconciler, in place of the template's closing line
about triggering a rollout.
4. **The repository does not declare it.** Stop with: "`<repo>` does not
declare `<kind>/<name>`, so a pull request cannot delete it: removing a
declaration is the only delete a pull request carries. Someone with write
access to `<cluster>` has to run the delete. I have changed nothing."
Block with that line as the reason. Do not write a manifest for it.
5. **Say what merging will do, from what you read.** Removing the file deletes
the object only if a reconciler syncs that directory and prunes. Read the
reconciler objects on the target cluster, and on the hub that deploys to it
when your kubeconfig reaches one (Argo CD usually runs on a hub). Your
default context is the cluster this agent runs on, not the target: pass
the target's own kubeconfig, `$HERMES_HOME/.kubeconfigs/kubeconfig_<project>_<cluster>_<location>.yaml`,
with `--kubeconfig` on every read. When that file is not there yet,
fetch the target's credentials into it first (**gke-basics**, "Cluster
Credentials"). An answer of
`the server doesn't have a resource type` means that reconciler is not
installed on the context you read, and on this agent's own cluster it
says nothing about the target. Match each object's source against
`<repo>` and the file's directory:
```bash
kubectl get applications.argoproj.io -A -o yaml --kubeconfig "<target kubeconfig>"
kubectl get kustomizations.kustomize.toolkit.fluxcd.io -A -o yaml --kubeconfig "<target kubeconfig>"
kubectl get rootsyncs.configsync.gke.io -A -o yaml --kubeconfig "<target kubeconfig>"
kubectl get reposyncs.configsync.gke.io -A -o yaml --kubeconfig "<target kubeconfig>"
```
- Argo CD prunes only with `spec.syncPolicy.automated.prune: true`; without
it the Application goes OutOfSync and someone syncs with prune. A
namespace Argo CD created through `CreateNamespace=true` is never pruned,
even with the Application deleted: for that shape, say merging leaves the
namespace in place.
- A Flux `Kustomization` prunes only with `spec.prune: true`.
- Config Sync deletes the objects it applied when their file goes.
- Pruning skips an object that opts out, so read the live object's
annotations (`kubectl get <kind> <name> -o yaml --kubeconfig "<target kubeconfig>"`) before saying it will
be deleted: `argocd.argoproj.io/sync-options: Prune=false`,
`kustomize.toolkit.fluxcd.io/prune: disabled`,
`configmanagement.gke.io/managed: disabled` or
`client.lifecycle.config.k8s.io/deletion: detach` (which Config Sync sets
on a namespace it created only because objects sat in it) each mean
merging leaves it in place. An object with no reconciler's labels or
annotations was put there some other way, and no prune touches it.
- **A reconciler that syncs the path but is failing still counts as
present.** A `RootSync` or `RepoSync` with entries under
`status.source.errors`, `status.rendering.errors` or `status.sync.errors`,
an Application whose sync status is `Unknown` or health `Degraded`, or a
Kustomization with `Ready=False`: name it, quote its error, and say
merging deletes nothing until it syncs again.
A `Forbidden` is not an absence: say you could not read the reconcilers,
and fall back to their CRDs, which this agent's role can read:
```bash
kubectl get crd applications.argoproj.io kustomizations.kustomize.toolkit.fluxcd.io rootsyncs.configsync.gke.io reposyncs.configsync.gke.io --kubeconfig "<target kubeconfig>"
```
Name a reconciler in the reply or the pull request body only if you found
it syncing that directory. When one prunes, syncs, and the object does
not opt out, say it will delete the object after merge. Never write that
merging "triggers" a delete you have not read a healthy reconciler for. Otherwise say: "Merging removes the declaration only. I found
nothing on `<cluster>` that deletes what is removed from `<repo>`, so
`<kind>/<name>` stays until someone deletes it."
6. **Words.** After the confirmation, say "I'll propose this as a pull request",
and report the pull request you opened and the files it removes. Never
describe the request as an action under way or done ("deleting",
"deleted"): the pull request only proposes it.
### Step 3: Call the Secure Submit Suggestion Script
The same helper with `submit` publishes the branch and opens the pull request —
or updates the one already open. It finds the working copy Step 1 made, so the
only thing to pass back is the branch name.
Write the description to a file first and pass the path. Inside double quotes
bash expands backticks and `$(...)`, and a pull request body is full of
backticks — through `--body` a benign one silently deletes its own text and a
hostile one runs in the working copy you are about to publish:
```bash
BODY=$(mktemp -p /opt/data/scratch pr_body.XXXXXX.md)
cat > "$BODY" <<'EOF'
This Pull Request was generated automatically by the **Platform Agent** control plane.
### 🚀 Functional Impact:
<detailed_markdown_bulleted_impact_description>
Please review the code diffs and merge this PR to trigger the GitOps CI/CD rollout!
EOF
```
The quoted `<<'EOF'` matters as much as `--body-file`: unquoted, the heredoc
expands the same constructs the argument would have. `mktemp` matters because
`/opt/data/scratch` is shared with every other card running right now, and a
fixed name there is two cards writing one file — one card's description on the
other's pull request. Keep the file directly in `/opt/data/scratch`, the only
directory `--body-file` reads from, and **not** inside the `workspace`: a file
written there is untracked, and `commit` refuses rather than guess — it names
the file and tells you to name the paths that belong in the change — so a
description left in the working copy stops the submit instead of riding along
in it.
```bash
python3 "$HERMES_HOME"/skills/submit-suggestion/scripts/submit_suggestion.py submit \
--repo "<owner>/<repo>" \
--branch "platform-agent/<change_type>-<target_id>" \
--title "<pr_title>" \
--body-file "$BODY"
```
`--base <branch>` names what the change merges into, for the rare case where it
is not the `base` Step 1 reported. It may not name the branch you are
submitting: a head branch that is its own base carries nothing for anyone to
review, and `prepare`, `submit` and the broker each refuse it. That covers the
repository whose trunk is called something other than `main` — the name is read
from the remote, not from a list. On a repository with a configured base, a new
pull request may only target that base: a `--base` naming anything else is
refused with `TARGET_NOT_BASE` before anything is published, and the message
names the configured base. If you passed `--base`, drop it and submit again.
If you did not, the copy was prepared before the base was configured or moved:
read the change in the old copy, run `prepare --force` with the same
`--branch`, reapply the change, and submit from the new copy. `submit` recorded
the change as a revision before the publish was refused, so a plain `prepare`
refuses to replace that copy. `--force` is safe here: the revision it deletes
is the one you have read and are about to reapply. Do not try another target,
and do not pass the named branch as `--base` from the old copy: it was cut from
another branch, and the broker refuses it `BASE_MOVED`.
The script returns the clean, live pull request URL. If a pull request for this
branch is already open, it updates that one's title and body in place and
returns its URL — resubmitting is not an error. `--keep-description` (Step 5) is
the one exception: it leaves the open pull request's title and body as their
author wrote them.
Older invocations carried `--workspace`, `--lease`, `--handle`, `--from`,
`--delete` and `--base-sha`, and had `list` and `fetch` as commands of their
own. The flags are still accepted, and read and ignored with a line saying so;
the two commands refuse with a line saying where the files are now. That is all
any of it buys: a command written against the old shape fails on what is
actually wrong with it — there is no working copy here, take the branch with
`prepare` — rather than on "unrecognized arguments" or "invalid choice", which
say nothing and hide the real cause. A card that prepared before an upgrade
cannot submit after one; its clone was on a volume this script no longer has.
Prepare again. Do not write new commands with these flags.
### Step 4: Confirm Suggestion
Record the PR link returned by the script, update the pending status inside your local state registry (if applicable), and present a clean, human-readable confirmation containing the PR URL link back to the user.
### Step 5: Addressing Review Feedback on an Existing PR
When you are asked to **address review comments / reviewer feedback** on an
existing PR, **read the comments yourself — never expect them pasted into the
task.** The `version-control` skill reads and writes them through the same
broker this skill publishes through; there is no forge CLI to authenticate and
no token to refresh.
1. **Read the pull request and all its feedback** — the conversation and the
inline review comments arrive together, each carrying the file and line it
sits on where it has one:
```bash
V="$HERMES_HOME"/skills/version-control/scripts/vcs.py
python3 "$V" proposal view <PR_NUMBER> --repo "<owner>/<repo>" --comments
```
Add `--diff` for the change under discussion. The `source` field of that
answer is the branch to work on in the next step.
2. **Apply the requested changes on the PR's own branch.** Run Step 1 against
that branch — `prepare --repo "<owner>/<repo>" --branch <source>` — then
Steps 2 and 3 exactly as written. Two things differ from a first submission
and both are handled for you: the copy comes down with the revisions already
under review on it and yours go on top, and `submit` adds to the branch
rather than replacing it, refusing outright if somebody else has moved it in
the meantime.
Edit the files the reviewer commented on in the `workspace` — what is on the
branch is what you are being asked to change, and rewriting a file from
memory loses the rest of it. A Terraform, Config Connector or Helm-chart
file is edited by its authoring skill's rules here too (Step 2's table),
its checks included: `tofu fmt` after any `.tf` edit. Its Step 1 and its
submit do not apply: stay on `<source>` in this copy. Stage only those specific files
(**never `git add .` / `-A`**).
Pass `--title` and `--body-file` again so the description matches the
revisions now on the branch — or `--keep-description` and no body-file, when
what you were asked for does not alter what the pull request is for. That
flag keeps the title along with the body, so a `--title` passed beside it
does **not** reach the pull request, and the run says so. It is not inert,
though: it is still the message any uncommitted edits are recorded under, so
pass the one you would want on that revision and not a placeholder. Pass one
whenever you left edits uncommitted at Step 2 — without it the run is refused
for having changes and no message to record them under. `--keep-description`
also needs the pull request to still be open: a merged or closed one is not a
description to keep, and the script refuses before publishing anything rather
than opening a fresh pull request with no description at all.
3. **Reply on the PR** summarizing what changed, then relay a clean
confirmation (PR URL + what you changed) back through your kanban result.
Through a file, for the reason Step 3 gives. A reply is a summary of edits
you just made and often quotes the comment you are answering — backticks in
your own text, and `$(...)` in somebody else's. `proposal comment` takes no
`--body-file`, so the file is read by a command substitution you write
yourself; what it expands to is an argument and is not expanded again.
```bash
REPLY=$(mktemp -p /opt/data/scratch pr_reply.XXXXXX.md)
cat > "$REPLY" <<'EOF'
<what changed>
EOF
python3 "$V" proposal comment <PR_NUMBER> --repo "<owner>/<repo>" --body "$(cat "$REPLY")"
```
Never ask the requester to paste the comment text — fetching it and addressing it is your job.