diff --git a/claude-skills/feature/SKILL.md b/claude-skills/feature/SKILL.md index 7ac8c85..c3dc48a 100644 --- a/claude-skills/feature/SKILL.md +++ b/claude-skills/feature/SKILL.md @@ -63,6 +63,14 @@ in the current project directory when needed, then creates FluencyLoop's state. continuing. Only stop if `init` itself fails. Do not hand-create `.fluencyloop`, `docs`, or `.claude/skills`. +**Reattach a safe detached checkout.** If `branch` is `HEAD`, `state_matches_branch` is `true`, +and `state_branch` is nonempty, this is the exact recorded feature tip, not a split. Run `git +status --porcelain`. If it is empty, run `git checkout `, then rerun the bundled +`fluencyloop check --json` before any work. If it is not empty, stop and state that the detached +checkout has changes which must be resolved before it can be reattached. Do not write records or +start a new feature while detached. `check` deliberately treats only the exact branch tip this +way; any other detached commit remains a split. + **Refuse split state.** Parse `state_matches_branch` from the bundled `fluencyloop check --json` before any calibration, feature declaration, session, or writer command. If it is `false`, stop immediately: state the checkout branch and `state_branch`, and ask the developer which existing @@ -356,8 +364,11 @@ Build the feature one **meaningful slice** at a time (a logical, commit-worthy c `documented`; use `follow-up` only when appropriate. Separate it from decisions: a role you explained is knowledge transfer even if no fork was chosen. **About the work, never the person** — no competence, prior knowledge, or "who learned what" (committed files, GDPR); the - per-developer picture lives only in the calibration profile. Escape a literal `|` as `\|` and a - literal backslash as `\\`. + per-developer picture lives only in the calibration profile. Compose and validate every value + before the one batched call: each `--component` has exactly three or four **nonempty** + pipe-separated fields and each `--gotcha` has exactly two **nonempty** fields. Omit a field + instead of emitting `||`. Escape a literal `|` as `\|` and a literal backslash as `\\`. Do not + run incomplete, bare, or trial `fluencyloop knowledge` commands to discover this syntax. - **Decisions** *(the script formats them — you supply only the field values)* — for each, run `fluencyloop decision` so the block is assembled deterministically; never hand-write the bullet schema: @@ -451,6 +462,14 @@ and the store records already carry that detail. Read `fluencyloop calibration s at distillation time, and pitch the prose to the developer's recorded level in the relevant domain: +**Make the coverage decision explicit.** Re-read the feature's completed slices before writing. +If they establish or change a product idea that a new joiner needs to hold, append its +architectural record with one to three useful tags and create its explanation. If they establish +the project's initial product shape or materially change its problem, shape, or major flow, +refresh `product.md`. The first material feature is never an excuse to leave both blank. A wholly +local feature may omit those two artifacts, but say why in the hand-off rather than silently +skipping the decision. + - `fluent` / `familiar`: concise product-level prose; name the load-bearing change without re-teaching fundamentals. - `learning` / `new`: explain the terms, flow, and product consequence needed to hold the diff --git a/plugins/fluencyloop/scripts/bash/calibration.sh b/plugins/fluencyloop/scripts/bash/calibration.sh index 96e3997..f00d100 100755 --- a/plugins/fluencyloop/scripts/bash/calibration.sh +++ b/plugins/fluencyloop/scripts/bash/calibration.sh @@ -121,6 +121,9 @@ reset_signals() { SUB="${1:-show}"; shift || true case "$SUB" in + help|-h|--help) + echo "Usage: fluencyloop calibration [init | show [--json] | edit | signal | compact [--dry-run]]" + ;; init) if [ -f "$CAL" ]; then echo "Calibration profile already exists: $CAL" else seed; echo "Created calibration profile: $CAL"; fi diff --git a/plugins/fluencyloop/scripts/bash/check.sh b/plugins/fluencyloop/scripts/bash/check.sh index 77e0d4a..8f629eb 100755 --- a/plugins/fluencyloop/scripts/bash/check.sh +++ b/plugins/fluencyloop/scripts/bash/check.sh @@ -42,8 +42,18 @@ STAGE="$(state_get stage)" BASE="$(state_get base_ref)"; [ -z "$BASE" ] && BASE="main" LAST_SESSION="$(state_get last_session)" STATE_MATCHES_BRANCH=true +DETACHED_HEAD=false +[ "$BRANCH" = "HEAD" ] && DETACHED_HEAD=true if [ -n "$STATE_BRANCH" ] && [ -n "$BRANCH" ] && [ "$STATE_BRANCH" != "$BRANCH" ]; then - STATE_MATCHES_BRANCH=false + # A detached checkout at the recorded branch tip is a recoverable transport state (for + # example after an agent has checked out a commit). It is not a conflicting feature context. + # Do not accept an arbitrary ancestor: attaching to it could silently discard newer branch work. + if $DETACHED_HEAD && git show-ref --verify --quiet "refs/heads/$STATE_BRANCH" \ + && [ "$(git rev-parse HEAD 2>/dev/null || true)" = "$(git rev-parse "$STATE_BRANCH" 2>/dev/null || true)" ]; then + : # The feature skill reattaches this exact, clean branch tip before it writes work. + else + STATE_MATCHES_BRANCH=false + fi fi # --- un-journaled drift: commits since the last committed session record. Before the first @@ -249,12 +259,13 @@ if [ -n "$STORE_ROOT" ] && [ -d "$STORE_ROOT" ]; then fi if $JSON_MODE; then - printf '{"git_repo":%s,"fluency":%s,"legacy_imported_features":%s,"legacy_migration_pending":%s,"branch":"%s","state_branch":"%s","state_matches_branch":%s,"feature":"%s","stage":"%s","base_ref":"%s","last_session":"%s","unjournaled_commits":%s,"calibration":%s,"node":%s,"constitution":"%s","store_errors":[%s]}\n' \ + printf '{"git_repo":%s,"fluency":%s,"legacy_imported_features":%s,"legacy_migration_pending":%s,"branch":"%s","detached_head":%s,"state_branch":"%s","state_matches_branch":%s,"feature":"%s","stage":"%s","base_ref":"%s","last_session":"%s","unjournaled_commits":%s,"calibration":%s,"node":%s,"constitution":"%s","store_errors":[%s]}\n' \ "$IN_GIT_REPO" \ "$FLUENCY_PRESENT" \ "$LEGACY_IMPORTED_FEATURES" \ "$LEGACY_MIGRATION_PENDING" \ "$(json_escape "$BRANCH")" \ + "$DETACHED_HEAD" \ "$(json_escape "$STATE_BRANCH")" \ "$STATE_MATCHES_BRANCH" \ "$(json_escape "$FEATURE")" \ diff --git a/plugins/fluencyloop/scripts/powershell/calibration.ps1 b/plugins/fluencyloop/scripts/powershell/calibration.ps1 index 8e61781..c69d903 100644 --- a/plugins/fluencyloop/scripts/powershell/calibration.ps1 +++ b/plugins/fluencyloop/scripts/powershell/calibration.ps1 @@ -110,6 +110,9 @@ $rest = @() if ($args.Count -gt 1) { $rest = @($args[1..($args.Count - 1)]) } switch ($sub) { + 'help' { FlOut 'Usage: fluencyloop calibration [init | show [--json] | edit | signal | compact [--dry-run]]' } + '-h' { FlOut 'Usage: fluencyloop calibration [init | show [--json] | edit | signal | compact [--dry-run]]' } + '--help' { FlOut 'Usage: fluencyloop calibration [init | show [--json] | edit | signal | compact [--dry-run]]' } 'init' { if (Test-Path -LiteralPath $CAL) { FlOut "Calibration profile already exists: $CAL" } else { Seed; FlOut "Created calibration profile: $CAL" } diff --git a/plugins/fluencyloop/scripts/powershell/check.ps1 b/plugins/fluencyloop/scripts/powershell/check.ps1 index f402832..930fcc1 100644 --- a/plugins/fluencyloop/scripts/powershell/check.ps1 +++ b/plugins/fluencyloop/scripts/powershell/check.ps1 @@ -30,6 +30,20 @@ $stage = FlStateGet 'stage' $base = FlStateGet 'base_ref'; if (-not $base) { $base = 'main' } $lastSession = FlStateGet 'last_session' $stateMatchesBranch = (-not $stateBranch) -or (-not $branch) -or ($stateBranch -eq $branch) +$detachedHead = $branch -eq 'HEAD' +if (-not $stateMatchesBranch -and $detachedHead) { + # Treat only the exact recorded branch tip as recoverable. An arbitrary ancestor could be an + # intentional historical checkout, so it remains a real split state requiring user direction. + & git show-ref --verify --quiet "refs/heads/$stateBranch" *> $null + if ($LASTEXITCODE -eq 0) { + $headCommit = & git rev-parse HEAD 2>$null + $stateCommit = & git rev-parse $stateBranch 2>$null + if ($LASTEXITCODE -eq 0 -and $headCommit -and $stateCommit -and + (($headCommit | Select-Object -First 1) -eq ($stateCommit | Select-Object -First 1))) { + $stateMatchesBranch = $true + } + } +} # Un-journaled drift: commits since the last committed session record; else since base. Legacy # session markdown remains a read-only fallback for projects that have not imported it yet. @@ -188,6 +202,7 @@ if ($jsonMode) { ',"legacy_imported_features":' + $legacyImportedFeatures + ',"legacy_migration_pending":' + $legacyMigrationPending.ToString().ToLowerInvariant() + ',"branch":"' + (FlJsonEscape $branch) + '"' + + ',"detached_head":' + $detachedHead.ToString().ToLowerInvariant() + ',"state_branch":"' + (FlJsonEscape $stateBranch) + '"' + ',"state_matches_branch":' + $stateMatchesBranch.ToString().ToLowerInvariant() + ',"feature":"' + (FlJsonEscape $feature) + '"' + diff --git a/plugins/fluencyloop/skills/feature/SKILL.md b/plugins/fluencyloop/skills/feature/SKILL.md index 22991ea..5d6563d 100644 --- a/plugins/fluencyloop/skills/feature/SKILL.md +++ b/plugins/fluencyloop/skills/feature/SKILL.md @@ -65,6 +65,14 @@ For that `fluencyloop init --json` command in Codex, request sandbox elevation b execution. It may create or update Codex-protected `.git` metadata; do not first attempt it in the standard sandbox. +**Reattach a safe detached checkout.** If `branch` is `HEAD`, `state_matches_branch` is `true`, +and `state_branch` is nonempty, this is the exact recorded feature tip, not a split. Run `git +status --porcelain`. If it is empty, run `git checkout `, then rerun `fluencyloop +check --json` before any work. If it is not empty, stop and state that the detached checkout has +changes which must be resolved before it can be reattached. Do not write records or start a new +feature while detached. `check` deliberately treats only the exact branch tip this way; any other +detached commit remains a split. + **Refuse split state.** Parse `state_matches_branch` from `fluencyloop check --json` before any calibration, feature declaration, session, or writer command. If it is `false`, stop immediately: state the checkout branch and `state_branch`, and ask the developer which existing feature context @@ -413,8 +421,11 @@ visible; the journal is its durable byproduct. `documented`; use `follow-up` only when appropriate. Separate it from decisions: a role you explained is knowledge transfer even if no fork was chosen. **About the work, never the person** — no competence, prior knowledge, or "who learned what" (committed files, GDPR); the - per-developer picture lives only in the calibration profile. Escape a literal `|` as `\|` and a - literal backslash as `\\`. + per-developer picture lives only in the calibration profile. Compose and validate every value + before the one batched call: each `--component` has exactly three or four **nonempty** + pipe-separated fields and each `--gotcha` has exactly two **nonempty** fields. Omit a field + instead of emitting `||`. Escape a literal `|` as `\|` and a literal backslash as `\\`. Do not + run incomplete, bare, or trial `fluencyloop knowledge` commands to discover this syntax. - **Decisions** *(the script formats them — you supply only the field values)* — for each, run `fluencyloop decision` so the block is assembled deterministically; never hand-write the bullet schema: @@ -506,6 +517,14 @@ and the store records already carry that detail. Read `fluencyloop calibration s at distillation time, and pitch the prose to the developer's recorded level in the relevant domain: +**Make the coverage decision explicit.** Re-read the feature's completed slices before writing. +If they establish or change a product idea that a new joiner needs to hold, append its +architectural record with one to three useful tags and create its explanation. If they establish +the project's initial product shape or materially change its problem, shape, or major flow, +refresh `product.md`. The first material feature is never an excuse to leave both blank. A wholly +local feature may omit those two artifacts, but say why in the hand-off rather than silently +skipping the decision. + - `fluent` / `familiar`: concise product-level prose; name the load-bearing change without re-teaching fundamentals. - `learning` / `new`: explain the terms, flow, and product consequence needed to hold the diff --git a/tests/calibration.bats b/tests/calibration.bats index e88fc75..bdebf3b 100644 --- a/tests/calibration.bats +++ b/tests/calibration.bats @@ -63,6 +63,12 @@ assert "bogus" not in d, d # invalid level excluded [ "$status" -ne 0 ] } +@test "--help prints calibration usage successfully" { + run cal --help + [ "$status" -eq 0 ] + [[ "$output" == *"Usage: fluencyloop calibration"* ]] +} + # --- signals + compact (demonstrated-engagement adaptation) --------------- level_of() { cal show --json | python3 -c "import json,sys;print(json.load(sys.stdin).get('$1',''))"; } diff --git a/tests/check.bats b/tests/check.bats index 13270d5..2c83a91 100644 --- a/tests/check.bats +++ b/tests/check.bats @@ -31,6 +31,18 @@ load test_helper [ "$(echo "$output" | json_field state_branch)" = "feature/001-add-search" ] } +@test "check accepts a detached checkout at the recorded feature tip" { + setup_initialized_repo + bash "$BIN/new-feature.sh" "add search" >/dev/null + git checkout -q --detach HEAD + + run bash "$BIN/check.sh" --json + [ "$status" -eq 0 ] + [ "$(echo "$output" | json_field branch)" = "HEAD" ] + [ "$(echo "$output" | json_field detached_head)" = "True" ] || [ "$(echo "$output" | json_field detached_head)" = "true" ] + [ "$(echo "$output" | json_field state_matches_branch)" = "True" ] || [ "$(echo "$output" | json_field state_matches_branch)" = "true" ] +} + @test "check: constitution states - present and pointer" { setup_initialized_repo printf '# Constitution\n\n## Principles\n\n### §1 — no sync calls in the request path\n' \ diff --git a/tests/plugin.bats b/tests/plugin.bats index 9f4e1fa..e098eb0 100644 --- a/tests/plugin.bats +++ b/tests/plugin.bats @@ -96,6 +96,8 @@ feature_text = read_text(root / "claude-skills" / "feature" / "SKILL.md") assert "**Refuse split state.**" in feature_text assert "state_matches_branch" in feature_text assert "Do not create a new feature, switch branches, or overwrite state" in feature_text +assert "Reattach a safe detached checkout." in feature_text +assert "git checkout " in feature_text assert "Resume preconditions after the answer." in feature_text assert "Immediately rerun `fluencyloop check --json`" in feature_text assert "**Migrate imported history before normal feature work.**" in feature_text @@ -117,6 +119,8 @@ codex_feature_text = read_text(dist / "skills" / "feature" / "SKILL.md") assert "**Refuse split state.**" in codex_feature_text assert "state_matches_branch" in codex_feature_text assert "Do not create a new feature, switch branches, or overwrite state" in codex_feature_text +assert "Reattach a safe detached checkout." in codex_feature_text +assert "git checkout " in codex_feature_text assert "Resume preconditions after the answer." in codex_feature_text assert "Immediately rerun `fluencyloop check --json`" in codex_feature_text assert "complete the mandatory migration before calibration, preferences, ticket numbering" in codex_feature_text @@ -146,6 +150,10 @@ assert "topic-specific question" in codex_feature_text assert "standalone comprehension question" not in codex_feature_text assert "do not run another implementation" in codex_feature_text for feature_skill_text in [feature_text, codex_feature_text]: + assert "exactly three or four **nonempty**" in feature_skill_text + assert "Do not\n run incomplete, bare, or trial `fluencyloop knowledge`" in feature_skill_text + assert "Make the coverage decision explicit." in feature_skill_text + assert "The first material feature is never an excuse" in feature_skill_text assert "Understanding checks are self-report, never quizzes" in feature_skill_text assert "Do you understand this explanation, or should I clarify anything?" in feature_skill_text assert 'explaining it "in your own' in feature_skill_text diff --git a/tests/powershell/calibration.Tests.ps1 b/tests/powershell/calibration.Tests.ps1 index bf907aa..39c7994 100644 --- a/tests/powershell/calibration.Tests.ps1 +++ b/tests/powershell/calibration.Tests.ps1 @@ -47,6 +47,11 @@ Describe 'calibration.ps1' { (Invoke-FlExit 'calibration.ps1' 'frobnicate') | Should -Not -Be 0 } + It '--help prints calibration usage successfully' { + (Invoke-FlExit 'calibration.ps1' '--help') | Should -Be 0 + (Invoke-Fl 'calibration.ps1' '--help') | Should -Match 'Usage: fluencyloop calibration' + } + It 'signal appends to the ledger; a calibration level is rejected as a signal' { & $script:PwshExe -NoProfile -File "$script:Bin/calibration.ps1" 'init' | Out-Null & $script:PwshExe -NoProfile -File "$script:Bin/calibration.ps1" 'signal' 'java' 'wave' | Out-Null diff --git a/tests/powershell/check.Tests.ps1 b/tests/powershell/check.Tests.ps1 index 321b0d3..b2da660 100644 --- a/tests/powershell/check.Tests.ps1 +++ b/tests/powershell/check.Tests.ps1 @@ -31,6 +31,17 @@ Describe 'check.ps1' { $j.state_branch | Should -Be 'feature/001-add-search' } + It 'accepts a detached checkout at the recorded feature tip' { + $script:repo = Initialize-TestRepo + & $script:PwshExe -NoProfile -File "$script:Bin/new-feature.ps1" 'add search' | Out-Null + git checkout -q --detach HEAD + (Invoke-FlExit 'check.ps1' '--json') | Should -Be 0 + $j = Get-FlJson 'check.ps1' '--json' + $j.branch | Should -Be 'HEAD' + $j.detached_head | Should -BeTrue + $j.state_matches_branch | Should -BeTrue + } + It 'constitution states: present and pointer' { $script:repo = Initialize-TestRepo $c = "$script:repo/docs/fluencyloop/constitution.md"