This is an automated email from the ASF dual-hosted git repository.

potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow-steward.git


The following commit(s) were added to refs/heads/main by this push:
     new 07a94a6d feat(spec-loop): scope update runs incrementally from a 
.last-sync marker (#467)
07a94a6d is described below

commit 07a94a6dca81f5775a8a6459d7a9cba17090032a
Author: Justin Mclean <[email protected]>
AuthorDate: Thu Jun 11 22:54:35 2026 +1000

    feat(spec-loop): scope update runs incrementally from a .last-sync marker 
(#467)
    
    * improved loop update
    
    * fix minor bug
    
    * make the branch name unique per run
---
 tools/spec-loop/.last-sync       |   2 +-
 tools/spec-loop/PROMPT_update.md |  46 +++++++++++++-----
 tools/spec-loop/loop.sh          | 102 ++++++++++++++++++++++++++++++++++++++-
 3 files changed, 134 insertions(+), 16 deletions(-)

diff --git a/tools/spec-loop/.last-sync b/tools/spec-loop/.last-sync
index 52b6b76e..a65a14fc 100644
--- a/tools/spec-loop/.last-sync
+++ b/tools/spec-loop/.last-sync
@@ -1 +1 @@
-043b48d5e56e30ab84f83da92c50566f471e18fe
+e4f79fc8d2470f2120ee5b6df1215d0f8159874f
diff --git a/tools/spec-loop/PROMPT_update.md b/tools/spec-loop/PROMPT_update.md
index be5c2174..5ea7d317 100644
--- a/tools/spec-loop/PROMPT_update.md
+++ b/tools/spec-loop/PROMPT_update.md
@@ -16,14 +16,26 @@ Context to load first:
 
 Steps:
 
-1. **Create the sync branch off the integration base**, then switch to
-   it: `git checkout -b sync-specs`. (One reviewable PR for the
-   sync.) Never commit the sync to the integration branch.
-2. Inventory the code with parallel subagents:
+1. **Check the `## Incremental scope` section appended below by the
+   runner.** If it names a previous sync commit, run the `git diff
+   --name-only` command it provides and treat that file list as the
+   *only* surface to re-audit — everything else is already in sync as of
+   that commit. If the diff is empty, exit without creating a branch or
+   commit (print "specs already in sync as of <SHA>"). If no previous
+   sync commit is recorded, fall through to a full inventory.
+2. **Create a uniquely-named sync branch off the integration base**, then
+   switch to it: `git checkout -b "sync-specs-$(date +%Y%m%d-%H%M%S)"`. A
+   fresh branch every run keeps each sync as its own reviewable PR and
+   never collides with or commits on top of a previous `sync-specs*`
+   branch. Note the exact name you created — you will print it in the
+   human-run commands below. Never commit the sync to the integration
+   branch.
+3. Inventory the code with parallel subagents (full inventory only if
+   step 1 did not narrow the surface):
    - every `.claude/skills/*/SKILL.md` (name, mode, what it does);
    - every `tools/*` project (what it does, its tests);
    - the mode/status table in `docs/modes.md`.
-3. Diff that inventory against `tools/spec-loop/specs/`:
+4. Diff that inventory against `tools/spec-loop/specs/`:
    - **New functionality with no spec** → author a new topic-named spec
      (no number prefix) following the format in
      [`specs/README.md`](specs/README.md), grounded in the real code it
@@ -35,25 +47,33 @@ Steps:
      are reflected).
    - **Removed functionality** → mark the spec or move it to a `Known
      gaps`/retired note; do not silently delete history.
-4. Update `specs/overview.md` and `specs/README.md` indexes if areas were
+5. Update `specs/overview.md` and `specs/README.md` indexes if areas were
    added or renamed.
-5. `git add -A` then `git commit` with subject
+6. `git add -A` then `git commit` with subject
    `docs(spec-loop): sync specs with contributed functionality` and a
-   `Generated-by: Claude (Opus 4.7)` trailer.
+   `Generated-by: Claude (Opus 4.7)` trailer. **Do NOT touch
+   `tools/spec-loop/.last-sync` yourself** — `loop.sh` amends the marker
+   into this commit after you finish, so the next `update` run knows to
+   scope from `$BASE_HEAD`. Leaving it alone avoids merge conflicts with
+   that amendment.
 
 Then STOP. Do NOT push, do NOT open a PR. Print the human-run commands:
 
+(substitute `<sync-branch>` with the exact branch name you created in
+step 2)
+
 ```text
-git push -u origin sync-specs
-gh pr create --web --base <integration-base> --head sync-specs \
+git push -u origin <sync-branch>
+gh pr create --web --base <integration-base> --head <sync-branch> \
   --title "Sync specs with contributed functionality" --body-file <body>
 ```
 
 Rules:
 
-- **Edit specs only.** This beat changes `tools/spec-loop/specs/` (and
-  the indexes). It must NOT change any skill, tool, or doc outside the
-  spec directory — it documents reality, it does not alter it.
+- **Edit specs only.** This beat changes `tools/spec-loop/specs/` and
+  the indexes. It must NOT change any skill, tool, or doc outside the
+  spec directory — it documents reality, it does not alter it. The
+  marker file `.last-sync` is owned by `loop.sh`; do not touch it.
 - Confirm with a code search before recording something as present or
   absent. Do not invent behaviour the code does not have.
 - Keep the RFCs untouched — they are a separate governance layer.
diff --git a/tools/spec-loop/loop.sh b/tools/spec-loop/loop.sh
index 966c5eef..aa855bb5 100755
--- a/tools/spec-loop/loop.sh
+++ b/tools/spec-loop/loop.sh
@@ -87,6 +87,10 @@ TOOLING_REF="${TOOLING_REF:-HEAD}"
 AGENT="${SPEC_LOOP_AGENT:-claude}"
 MODEL="${SPEC_LOOP_MODEL:-sonnet}"
 PR_LIMIT="${SPEC_LOOP_PR_LIMIT:-100}"
+# Agent output format. Default `text` is what the spinner expects; switch to
+# `stream-json` (SPEC_LOOP_OUTPUT_FORMAT=stream-json) to see live tool-call
+# events when debugging a slow or wedged run.
+OUTPUT_FORMAT="${SPEC_LOOP_OUTPUT_FORMAT:-text}"
 # Plan length that triggers ONE consolidation round before building. The
 # consolidate beat preserves every planned work item, so a plan that is long
 # because of *pending work* (not stale history) cannot shrink below this —
@@ -215,6 +219,50 @@ open_pr_context() {
 # work is what stops the agent re-picking the same top-priority plan item and
 # rebuilding it on a new branch every iteration. Reads refs only, so it is
 # correct regardless of which branch is currently checked out.
+# Incremental scope for `update`: read the saved sync marker and tell the
+# agent to only re-inspect paths that changed since then. Without this the
+# update beat re-audits every skill, tool, and modes.md row on every run,
+# which is the bulk of the streaming volume on a slow link.
+#
+# The marker is `tools/spec-loop/.last-sync` — a plain file containing the
+# BASE SHA the specs were last synced against. Read from the working tree
+# (so a half-finished sync counts); fall back to the control branch via
+# `git show` for the case where BASE is already checked out. The update
+# prompt overwrites this file at the end of every successful sync.
+update_scope_context() {
+    echo ""
+    echo "## Incremental scope — only re-inspect what changed since the last 
sync"
+    echo ""
+    local marker="tools/spec-loop/.last-sync"
+    local prev
+    if [ -f "$marker" ]; then
+        prev="$(tr -d '[:space:]' < "$marker")"
+    else
+        prev="$(git show "$TOOLING_REF:$marker" 2>/dev/null | tr -d 
'[:space:]')"
+    fi
+    if [ -z "$prev" ]; then
+        echo "No \`$marker\` recorded — do a full inventory, then write the 
new"
+        echo "BASE SHA to that file as part of the sync commit."
+        return 0
+    fi
+    echo "The last sync marker (\`$marker\`) is \`$prev\`. The current BASE 
HEAD"
+    echo "is \`$BASE_HEAD\`."
+    echo ""
+    echo "Scope your inventory to paths touched in that range:"
+    echo ""
+    echo '```'
+    echo "git diff --name-only $prev..$BASE_HEAD -- .claude/skills tools 
docs/modes.md"
+    echo '```'
+    echo ""
+    echo "Skills, tools, and \`docs/modes.md\` rows untouched in that range 
are still"
+    echo "in sync as of the previous run — skip them. Only re-inspect specs 
whose"
+    echo "subjects appear in the diff. If the diff is empty there is nothing 
to"
+    echo "sync; exit without creating a branch or commit."
+    echo ""
+    echo "When you finish the sync, overwrite \`$marker\` with \`$BASE_HEAD\` 
and"
+    echo "include it in the sync commit so the next run picks up from here."
+}
+
 local_branch_context() {
     echo ""
     echo "## Local work-item branches"
@@ -311,7 +359,11 @@ while true; do
         echo "Error: could not read '$ACTIVE_PROMPT' from the working tree or 
control branch '$TOOLING_REF'." >&2
         rm -f "$PROMPT_WITH_CONTEXT"; break
     fi
-    open_pr_context >> "$PROMPT_WITH_CONTEXT"
+    # Update mode just diffs code against specs; it doesn't pick a work item, 
so
+    # the open-PR list (a network round-trip via gh) buys nothing. Skip it 
there.
+    if [ "$MODE" != "update" ]; then
+        open_pr_context >> "$PROMPT_WITH_CONTEXT"
+    fi
     local_branch_context >> "$PROMPT_WITH_CONTEXT"
 
     if [ "$BUILD_ITERATION" = true ]; then
@@ -358,6 +410,13 @@ while true; do
             fi
         fi
         BASE_HEAD="$(git rev-parse HEAD)"
+
+        # Update-only: append incremental scope now that BASE is checked out
+        # and BASE_HEAD is known. The agent will diff $prev..$BASE_HEAD on the
+        # listed paths and skip anything not touched in that range.
+        if [ "$MODE" = "update" ]; then
+            update_scope_context >> "$PROMPT_WITH_CONTEXT"
+        fi
     fi
 
     # Run one iteration with a fresh context.
@@ -369,10 +428,15 @@ while true; do
     #   --disallowedTools …             defense-in-depth: hard-deny push and
     #                                   gh so a stray call cannot reach the
     #                                   remote even with permissions skipped.
+    # Claude CLI requires --verbose with -p when output-format is stream-json;
+    # add it only in that case to keep the default `text` run quiet.
+    VERBOSE_ARGS=()
+    [ "$OUTPUT_FORMAT" = "stream-json" ] && VERBOSE_ARGS=(--verbose)
     "$AGENT" -p \
         --dangerously-skip-permissions \
         --disallowedTools "Bash(git push:*)" "Bash(gh:*)" \
-        --output-format=text \
+        --output-format="$OUTPUT_FORMAT" \
+        ${VERBOSE_ARGS[@]+"${VERBOSE_ARGS[@]}"} \
         --model "$MODEL" < "$PROMPT_WITH_CONTEXT" &
     AGENT_PID=$!
     spinner "$AGENT_PID" & SPINNER_PID=$!
@@ -395,6 +459,40 @@ while true; do
             echo "⚠ No work-item branch was created (still on '$CUR_BRANCH'). 
Check the agent output above." >&2
         fi
 
+        # Update mode: advance the .last-sync marker to $BASE_HEAD, so the
+        # next `update` run scopes from here. Bundle the marker bump into
+        # the agent's sync commit when there is one (so it ships in the
+        # same PR); otherwise — if the agent saw nothing to sync and stayed
+        # on BASE — make a tiny marker-only branch off BASE.
+        if [ "$MODE" = "update" ]; then
+            if [ "$CUR_BRANCH" != "$BASE" ] && [ "$CUR_BRANCH" != 
"$TOOLING_REF" ]; then
+                printf '%s\n' "$BASE_HEAD" > tools/spec-loop/.last-sync
+                if ! git diff --quiet -- tools/spec-loop/.last-sync 
2>/dev/null; then
+                    git add tools/spec-loop/.last-sync
+                    if git commit --amend --no-edit >/dev/null 2>&1; then
+                        echo "[ marker ] amended .last-sync = $BASE_HEAD into 
$CUR_BRANCH"
+                    else
+                        echo "⚠ Could not amend .last-sync into '$CUR_BRANCH' 
— bump it by hand." >&2
+                    fi
+                fi
+            elif [ "$CUR_BRANCH" = "$BASE" ]; then
+                cur_marker=""
+                [ -f tools/spec-loop/.last-sync ] && cur_marker="$(tr -d 
'[:space:]' < tools/spec-loop/.last-sync)"
+                if [ "$cur_marker" != "$BASE_HEAD" ]; then
+                    marker_branch="advance-last-sync-${BASE_HEAD:0:7}"
+                    if git checkout -b "$marker_branch" >/dev/null 2>&1; then
+                        printf '%s\n' "$BASE_HEAD" > tools/spec-loop/.last-sync
+                        git add tools/spec-loop/.last-sync
+                        if git commit -m "chore(spec-loop): advance .last-sync 
to $BASE_HEAD" >/dev/null 2>&1; then
+                            echo "[ marker ] advanced .last-sync on 
$marker_branch"
+                            echo "           push it with:  git push -u origin 
$marker_branch"
+                            CUR_BRANCH="$marker_branch"
+                        fi
+                    fi
+                fi
+            fi
+        fi
+
         # Safety guard: a build/update iteration must never commit to the base.
         if [ "$CUR_BRANCH" = "$BASE" ] && [ "$(git rev-parse HEAD)" != 
"$BASE_HEAD" ]; then
             echo "✗ This iteration committed to '$BASE' instead of a work-item 
branch." >&2

Reply via email to