Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion .github/workflows/proposal-shared.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ jobs:
echo "::error::schedule/workflow_dispatch-triggered runs must receive capacity_query, queue_query, and review_limit"
exit 1
fi
if [ -z "${{ secrets.model_credentials }}" ]; then
echo "::error::schedule/workflow_dispatch-triggered runs must receive the model_credentials secret -- Propose calls a model to decide field values, it no longer mocks them"
exit 1
fi
fi
# Verifies the triggering comment is the proposal's actual first/
# control comment, not just any comment containing matching checkbox
Expand Down Expand Up @@ -235,7 +239,33 @@ jobs:
queue_query: ${{ inputs.queue_query }}
review_limit: ${{ inputs.review_limit }}
github_token: ${{ secrets.token }}
# Field values are mocked, not agent-decided yet — tracked in #11.
- name: Gather proposal context
id: context
if: steps.dequeue.outputs.result == 'dequeued'
uses: rubykatzen/starcast/actions/proposal@main
with:
mode: propose-context
parent_issue_id: ${{ steps.dequeue.outputs.issue_id }}
regulations_repo: ${{ inputs.regulations_repo }}
regulations_path: ${{ inputs.regulations_path }}
prefix: ${{ inputs.prefix }}
github_token: ${{ secrets.token }}
- name: Set up Node.js
if: steps.dequeue.outputs.result == 'dequeued'
uses: actions/setup-node@v4
- name: Install Copilot CLI
if: steps.dequeue.outputs.result == 'dequeued'
run: npm install -g @github/copilot
- name: Decide proposal content
id: inference
if: steps.dequeue.outputs.result == 'dequeued'
uses: actions/ai-inference@v1
with:
prompt: ${{ steps.context.outputs.prompt }}
system-prompt: 'You propose structured field values for a GitHub Issue, constrained by consumer-owned regulations text. Respond with exactly one JSON object and nothing else -- no prose, no Markdown code fence.'
model: claude-sonnet-4.5
env:
COPILOT_GITHUB_TOKEN: ${{ secrets.model_credentials }}
- name: Create proposal
id: propose
if: steps.dequeue.outputs.result == 'dequeued'
Expand All @@ -245,6 +275,7 @@ jobs:
parent_issue_id: ${{ steps.dequeue.outputs.issue_id }}
prefix: ${{ inputs.prefix }}
proposal_type_name: ${{ inputs.proposal_type_name }}
model_response: ${{ steps.inference.outputs.response }}
entity_query: ${{ inputs.entity_query }}
propose_transition_mutation: ${{ inputs.propose_transition_mutation }}
github_token: ${{ secrets.token }}
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/proposal.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ jobs:
# secrets.GITHUB_TOKEN cannot mutate Projects V2 (no such permission
# scope exists for the auto-provisioned token) -- this caller needs a
# real PAT with organization Projects read/write, stored as
# PROJECTS_TOKEN.
# PROJECTS_TOKEN. Propose's model call needs a separate PAT with an
# active GitHub Copilot seat, stored as COPILOT_PAT.
handle:
uses: ./.github/workflows/proposal-shared.yml
with:
Expand All @@ -39,3 +40,4 @@ jobs:
mutation($entityId: ID!) { updateProjectV2ItemFieldValue(input: {projectId: "PVT_kwDOD5LpBM4Bfjz_", itemId: $entityId, fieldId: "PVTSSF_lADOD5LpBM4Bfjz_zhZ13FI", value: {singleSelectOptionId: "98236657"}}) { clientMutationId } }
secrets:
token: ${{ secrets.PROJECTS_TOKEN }}
model_credentials: ${{ secrets.COPILOT_PAT }}
23 changes: 17 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ commit SHA. Do not recommend `@main` for stable consumers.
enforces that `issue_comment` runs carry `issue_number`/`comment_id` and
that `schedule`/`workflow_dispatch` runs do not, and that
`schedule`/`workflow_dispatch` runs carry `capacity_query`/`queue_query`/
`review_limit`. On `issue_comment` runs, `validate` also verifies the
`review_limit`/the `model_credentials` secret. On `issue_comment` runs,
`validate` also verifies the
triggering comment is actually the proposal's first/control comment
(#63) via `mode: check-control-comment`, shared here rather than
duplicated across `apply`/`reject`/`distill`/`rework` since all four
Expand Down Expand Up @@ -95,12 +96,22 @@ commit SHA. Do not recommend `@main` for stable consumers.
starts with `prefix`, stripped, plus the always-available fixed set
(`title`, `body`, `type`, `parent`, `labels`) — the menu a caller
(in the future, an agent) picks from.
- `propose-context` gathers everything a model needs to decide a
proposal's content -- regulations text (fetched from
`regulations_repo`/`regulations_path`'s default branch via
`repository.object(expression: "HEAD:<path>")`), the candidate
issue's title/body, and the field catalog from `proposal_fields` --
into one self-contained prompt, output as `prompt`. Produces text
only; agnostic to whichever inference mechanism a caller wires in.
- `propose` creates a proposal issue against a parent: resolves the
Issue Type, creates the sub-issue, fills every discovered custom
field with a trivial type-appropriate mock value (`propose` does
not read regulations or call a model yet — real agent judgment is
tracked in #11), and posts the control comment
(`CONTROL_COMMENT_BODY`) as the first comment.
Issue Type, creates the sub-issue, writes whichever fields
`--model-response` (a JSON blob shaped `{"title", "body",
"fields": {...}}`, produced from `propose-context`'s prompt by a
real model call) included, and posts the control comment
(`CONTROL_COMMENT_BODY`) as the first comment. Does not decide
content itself -- an unknown field name in the response is a
warning and a skip, not an error, since the model's output isn't
trusted to match the catalog exactly.
- `propose`/`apply` optionally transition whatever "controlling
entity" tracks an issue's state (e.g. a `ProjectV2Item`) as it
moves through the lifecycle (#69): `entity_query` resolves the
Expand Down
22 changes: 16 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,25 @@
`content { ... on Issue { id } }`).
- `list-fields`: discovers the prefixed proposal Issue Fields available
in a repository.
- `propose-context`: gathers regulations text, the candidate issue's
title/body, and the field catalog into one self-contained prompt for
a model to decide proposal content from — output only, agnostic to
whichever inference mechanism a caller wires in.
- `propose`: creates a proposal issue against a parent — Issue Type,
sub-issue relationship, field values, and the control comment. Field
*values* are mocked (a trivial type-appropriate placeholder) rather
than agent-decided; real regulation-constrained judgment is tracked
in #11.
sub-issue relationship, field values, and the control comment. Takes
the content to write as a `--model-response` JSON blob
(`{"title", "body", "fields": {...}}`) rather than deciding it
itself; an unknown field name in the response is a warning and a
skip, not an error.
- `apply`/`reject`: unchanged behavior from the actions they replace
(below), now reached via `mode: apply`/`mode: reject`.
- `proposal-shared.yml`'s `propose` job now dequeues a candidate and
creates a (mocked) proposal for it, instead of a placeholder notice.
- `proposal-shared.yml`'s `propose` job now dequeues a candidate, asks a
model (via `actions/ai-inference` wrapping GitHub Copilot CLI,
requiring a `model_credentials` secret backed by a PAT with an active
Copilot seat) what to propose, and creates the proposal from its
response — instead of a placeholder notice. `validate` hard-errors on
schedule/`workflow_dispatch` runs missing `model_credentials`, the
same way it already does for `capacity_query`/`queue_query`.
- `entity_query`, `propose_transition_mutation`, and
`apply_transition_mutation` inputs on `proposal-shared.yml`: optionally
transition whatever "controlling entity" tracks an issue's state (e.g.
Expand Down
36 changes: 23 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,9 @@ history remains available in Git.
The current reusable workflows cover centralized Project intake for issues and
pull requests, plus explicit label-based issue routing. The proposal-as-issue
lifecycle described above (`Propose`/`Apply`/`Reject`/`Distill`/`Rework`) has a
working contract, `proposal-shared.yml`: `Apply`/`Reject` are fully
implemented, `Propose` dequeues and creates a proposal with mocked field
values (real agent judgment not wired in yet), and `Distill`/`Rework` remain
placeholders (tracked in #11).
working contract, `proposal-shared.yml`: `Apply`/`Reject`/`Propose` are fully
implemented (`Propose` decides field values via a real model call, not a
mock), and `Distill`/`Rework` remain placeholders (tracked in #86/#87).

## Reusable workflows

Expand Down Expand Up @@ -223,21 +222,32 @@ jobs:
empty/unset proposal field (title/body included) leaves the parent's field
untouched. `Reject` just closes the proposal. Both are idempotent:
re-running against an already-closed proposal is a no-op.
- **`Propose` dequeues and creates, with mocked content** (#55, #11) — on
each scheduled run it runs the consumer-supplied `capacity_query`, and if
the result is below `review_limit`, runs `queue_query` and takes its first
- **`Propose` dequeues, asks a model, and creates** (#55, #11) — on each
scheduled run it runs the consumer-supplied `capacity_query`, and if the
result is below `review_limit`, runs `queue_query` and takes its first
candidate; at capacity or an empty queue are both clean no-ops. Both
queries are consumer-owned GraphQL text: `capacity_query` must alias
exactly one scalar numeric field as `capacity`, `queue_query` exactly one
array-valued field as `queue` (each element at least `{ id }`), found by a
recursive walk matching alias name and value type — zero or more than one
match is a hard configuration error. Only one candidate is ever dequeued
per run, so no pagination/cursor state is needed between runs. When a
candidate is found, `Propose` creates the proposal issue against it —
Issue Type, sub-issue relationship, the control comment — but **fills every
field with a trivial mock value rather than a real agent decision**; wiring
in regulation-constrained judgment is tracked in #11. `Distill`/`Rework`
remain placeholders.
per run, so no pagination/cursor state is needed between runs.

Deciding what to propose and creating the proposal are two separate
steps, split the same way `Apply`/`Reject` are deterministic and
`Distill`/`Rework` are agent work: `propose-context` gathers the
regulations text, the candidate's title/body, and the catalog of
available fields into one self-contained prompt (schema-agnostic about
which inference mechanism reads it); the caller hands that prompt to a
model and gets back one JSON object (`{"title", "body", "fields": {...}}`);
`propose` takes that JSON as `--model-response` and does only the
mechanical part — Issue Type, sub-issue relationship, field writes, the
control comment. The dogfood caller (`.github/workflows/proposal.yml`)
wires this to GitHub Copilot CLI via `actions/ai-inference`, requiring a
PAT with an active Copilot seat (`model_credentials`/`COPILOT_PAT`) —
`validate` hard-errors on schedule/`workflow_dispatch` runs if it's
missing, the same way it does for `capacity_query`/`queue_query`.
`Distill`/`Rework` remain placeholders.
- **The control comment has one fixed template**, posted by `Propose`
immediately after creation: `- [ ] Apply`, `- [ ] Reject`,
`- [ ] Distill`, `- [ ] Rework`, nothing else. Before `Apply`/`Reject`/
Expand Down
22 changes: 20 additions & 2 deletions actions/proposal/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,16 @@ inputs:
description: 'Id of the comment to verify is the proposal''s first/control comment (check-control-comment; see #63)'
required: false
parent_issue_id:
description: 'GraphQL node id of the issue to propose against (propose)'
description: 'GraphQL node id of the issue to propose against (propose, propose-context)'
required: false
regulations_repo:
description: '"owner/repo" that owns the regulations document (propose-context)'
required: false
regulations_path:
description: 'Path to the regulations document inside regulations_repo, read from its default branch (propose-context)'
required: false
model_response:
description: 'JSON {"title", "body", "fields": {...}} decided by a model from propose-context''s prompt (propose)'
required: false
capacity_query:
description: 'GraphQL query text; must alias exactly one scalar numeric field as `capacity` (dequeue)'
Expand Down Expand Up @@ -55,6 +64,9 @@ outputs:
fields:
description: 'JSON field catalog (list-fields only)'
value: ${{ steps.run.outputs.fields }}
prompt:
description: 'Self-contained prompt for a model to decide proposal content (propose-context only)'
value: ${{ steps.run.outputs.prompt }}
summary:
description: 'Human-readable summary of the outcome'
value: ${{ steps.run.outputs.summary }}
Expand All @@ -70,6 +82,9 @@ runs:
ISSUE_NUMBER: ${{ inputs.issue_number }}
COMMENT_ID: ${{ inputs.comment_id }}
PARENT_ISSUE_ID: ${{ inputs.parent_issue_id }}
REGULATIONS_REPO: ${{ inputs.regulations_repo }}
REGULATIONS_PATH: ${{ inputs.regulations_path }}
MODEL_RESPONSE: ${{ inputs.model_response }}
CAPACITY_QUERY: ${{ inputs.capacity_query }}
QUEUE_QUERY: ${{ inputs.queue_query }}
REVIEW_LIMIT: ${{ inputs.review_limit }}
Expand All @@ -87,8 +102,11 @@ runs:
list-fields)
args+=(--repository "$REPOSITORY" --prefix "$PREFIX")
;;
propose-context)
args+=(--parent-issue-id "$PARENT_ISSUE_ID" --regulations-repo "$REGULATIONS_REPO" --regulations-path "$REGULATIONS_PATH" --prefix "$PREFIX")
;;
propose)
args+=(--parent-issue-id "$PARENT_ISSUE_ID" --prefix "$PREFIX" --type "$PROPOSAL_TYPE_NAME" --entity-query "$ENTITY_QUERY" --transition-mutation "$PROPOSE_TRANSITION_MUTATION")
args+=(--parent-issue-id "$PARENT_ISSUE_ID" --prefix "$PREFIX" --type "$PROPOSAL_TYPE_NAME" --model-response "$MODEL_RESPONSE" --entity-query "$ENTITY_QUERY" --transition-mutation "$PROPOSE_TRANSITION_MUTATION")
;;
apply)
args+=(--repository "$REPOSITORY" --issue-number "$ISSUE_NUMBER" --prefix "$PREFIX" --type "$PROPOSAL_TYPE_NAME" --entity-query "$ENTITY_QUERY" --transition-mutation "$APPLY_TRANSITION_MUTATION")
Expand Down
Loading