Skip to content
Merged
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
53 changes: 53 additions & 0 deletions .github/tests/test_detached_supervision.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,59 @@ def test_attach_leaves_repository_pristine_and_state_external(self) -> None:
status = self.helper_json("detached-status", "--repo", ".")
self.assertTrue(status["attached"] and status["verified"])

def test_bootstrap_oracle_is_credential_free_and_executes_exact_output(self) -> None:
attached = self.helper_json("attach", "--repo", ".", "--mode", "detached")
source_plan = self.repo / "request.md"
source_plan.write_text("# Source plan\n")
document = "# Synthetic artifact\n\n`rm -rf /` is inert documentation.\n"

prescription = json.loads(
self.run_helper(
"flow",
"bootstrap",
"--repo",
".",
"--feature",
"detached-bootstrap",
"--source-plan",
"request.md",
"--artifact",
"source-plan.md",
"--shell",
"posix",
"--json",
stdin=document,
).stdout
)
self.assertEqual(prescription["verification_status"], "VERIFIED")
self.assertEqual(prescription["supervision_mode"], "detached")
self.assertTrue(Path(prescription["helper_path"]).is_absolute())
self.assertNotIn(".product-loop/boatstack planning-write", prescription["planning_envelope"])

events = {
"cursor": {"hook_event_name": "beforeShellExecution", "command": prescription["planning_envelope"]},
"claude": {"hook_event_name": "PreToolUse", "tool_name": "Bash", "tool_input": {"command": prescription["planning_envelope"]}},
"codex": {"hook_event_name": "PreToolUse", "tool_name": "Bash", "tool_input": {"command": prescription["planning_envelope"]}},
"gemini": {"hook_event_name": "BeforeTool", "tool_name": "run_shell_command", "tool_input": {"command": prescription["planning_envelope"]}},
}
for host, event in events.items():
admitted = self.run_helper(
"ambient-safety-hook", "--host", host, "--repo", ".", stdin=json.dumps(event)
)
self.assertNotIn("deny", admitted.stdout.lower(), host)

executed = subprocess.run(
["bash", "-c", prescription["planning_envelope"]],
cwd=self.repo,
env=self._env(),
text=True,
capture_output=True,
)
self.assertEqual(executed.returncode, 0, executed.stdout + executed.stderr)
artifact = Path(attached["control_root"]) / ".product-loop" / "features" / "detached-bootstrap" / "source-plan.md"
self.assertEqual(artifact.read_text(), document)
self.assert_repo_uncontaminated()

def test_activate_installs_guard_preserving_user_hooks(self) -> None:
self.run_helper("attach", "--repo", ".", "--mode", "detached")

Expand Down
55 changes: 48 additions & 7 deletions .github/tests/test_repository_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,15 +127,56 @@ def anchors(document: Path) -> set[str]:
self.assertIn("<desc", value, name)

def test_public_examples_exclude_private_context(self) -> None:
paths = [
REPO / "docs" / "account-recovery-walkthrough.md",
RUNTIME / "testdata" / "reviewer-pr-body.md",
]
for path in paths:
value = path.read_text()
for private in ("Tax" + "Weave", "/Users/", "bigboateng", "cursor_password_reset_button_addition"):
private_values = (
"Tax" + "Weave",
"/" + "Users/apple/Documents/GitHub/" + "tax" + "weave",
"big" + "boateng",
"cursor_password_" + "reset_button_addition",
)
text_suffixes = {".go", ".json", ".md", ".ps1", ".py", ".sh", ".yaml", ".yml"}
for path in REPO.rglob("*"):
if not path.is_file() or ".git" in path.parts or path.suffix not in text_suffixes:
continue
value = path.read_text(errors="replace")
for private in private_values:
self.assertNotIn(private, value, path)

def test_executable_documentation_examples_use_registered_cli(self) -> None:
documents = [
REPO / "README.md",
REPO / "boatstack" / "SKILL.md",
*sorted((REPO / "docs").glob("*.md")),
*sorted((REPO / "boatstack" / "references").glob("*.md")),
]
command_pattern = re.compile(
r"(?:\.product-loop/boatstack(?:\.ps1)?|boatstack-helper)\s+"
r"(?P<verb>[a-z][a-z0-9-]*)(?:\s+(?P<subcommand>[a-z][a-z0-9-]*))?"
)
nested = {"flow", "insight", "retro"}
checked: dict[tuple[str, ...], str] = {}
for document in documents:
value = document.read_text().replace("\\\n", " ")
for match in command_pattern.finditer(value):
verb = match.group("verb")
subcommand = match.group("subcommand") if verb in nested else None
command = (verb, subcommand) if subcommand else (verb,)
line_end = value.find("\n", match.end())
if line_end < 0:
line_end = len(value)
segment = value[match.start():line_end]
flags = set(re.findall(r"--([a-z][a-z0-9-]*)", segment))
if command not in checked:
help_result = self.run_helper(*command, "--help", expected=2)
checked[command] = help_result.stdout + help_result.stderr
help_text = checked[command]
self.assertIn(f"Usage of {' '.join(command)}:", help_text, document)
for flag in flags:
self.assertRegex(help_text, rf"(?m)^ -{re.escape(flag)}(?:\s|$)", document)

public_guidance = "\n".join(document.read_text() for document in documents)
self.assertNotIn(".product-loop/boatstack planning-write", public_guidance)
self.assertNotIn(".product-loop\\boatstack.ps1' planning-write", public_guidance)

def test_export_and_drift_contract(self) -> None:
with tempfile.TemporaryDirectory() as temp:
target = Path(temp)
Expand Down
2 changes: 1 addition & 1 deletion boatstack/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ Before starting `/auto-plan` for a new feature, check `next-status --repo . --js
13. If Spec Kit is installed, use its constitution/specify/clarify/plan/tasks/analyze/checklist flow as an artifact generator. The canonical artifact contract remains authoritative.
14. For every planned validation, record the exact `criteria` it can support plus `run`, `origin`, `oracle`, and `independence`. Commands, automated tests, external checks, and named human review procedures are all valid forms, but an ambiguous claim without a threshold/rubric and authorized decision remains `BLOCKED`.
14. For every external write, record `affected_paths` plus side-effect kind, immutable target identity, reversibility, failure policy, and `destructive: false`. Reject ambiguous reset rollback or target names.
15. Write only Markdown feature artifacts, including the canonical structured `plan.md`. Author every feature artifact through the owned channel: pass the complete document to `.product-loop/boatstack planning-write --repo . --feature <feature> --artifact <known-name>` using the literal planning transport in `.product-loop/workflow.md` — a single-quoted heredoc in a POSIX shell or the UTF-8-scoped single-quoted here-string in PowerShell. This is the primary writer for `.product-loop/features/`, not a fallback, and it remains available after the planning latch denies raw writes. Send the complete envelope in one tool call. Never run the helper without input, split the envelope across calls, use an expansion-capable delimiter, target another repository or helper, or paste Markdown at a shell prompt. Put the authoritative JSON inside the marked Boatstack block and run `.product-loop/boatstack check-plan --plan <feature>/plan.md`; this command is read-only. The host's ordinary Markdown writer may be used only where the host explicitly permits it. Never use arbitrary shell redirection to evade a host write boundary.
15. Write only Markdown feature artifacts, including the canonical structured `plan.md`. For every artifact, send the selected feature, durable in-repo source plan, artifact name, target shell, and complete Markdown to the read-only `flow bootstrap` oracle through the current Boatstack operation entrypoint. Execute only its returned `planning_envelope`, unchanged. The oracle is the sole authority for embedded or detached helper identity, source-plan freshness, and literal shell rendering. Re-resolve it after `workspace-cut` from `destination_repository`. Never construct a `planning-write` command, split either literal envelope across calls, use an expansion-capable delimiter, target another repository or helper, or paste Markdown at a shell prompt. Put the authoritative JSON inside the marked Boatstack block and run the current Boatstack entrypoint's read-only `check-plan --plan <feature>/plan.md`. The host's ordinary Markdown writer may be used only where the host explicitly permits it. Never use arbitrary shell redirection to evade a host write boundary.
16. Keep implementation tasks separate from publication authority. Internal phases remain tasks inside one delivery slice. When the accepted outcome explicitly requires multiple PRs, declare ordered `delivery_slices`; assign every task exactly once and give each slice its own optional base/head branch contract. Plan approval approves this structure but never authorizes a push or PR.
17. End with a **draft**, never an implied approval. Do not generate executable task state, JSON artifacts, locks, or implementation changes from `auto-plan`.

Expand Down
232 changes: 232 additions & 0 deletions boatstack/bootstrap.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
package boatstack

import (
"fmt"
"os"
"path/filepath"
"strings"
)

const bootstrapPrescriptionSchemaVersion = 1

type BootstrapShell string

const (
BootstrapShellPOSIX BootstrapShell = "posix"
BootstrapShellPowerShell BootstrapShell = "powershell"
)

type BootstrapOptions struct {
Repo string
Feature string
SourcePlan string
Artifact string
Shell BootstrapShell
Document []byte
}

// BootstrapPrescription is the read-only, mode-aware answer for the first
// managed planning write. It binds creation intent, source-plan freshness, the
// selected worktree, and the exact helper into one literal shell envelope.
// control-law: bootstrap-command-authority-is-workspace-bound
type BootstrapPrescription struct {
SchemaVersion int `json:"schema_version"`
VerificationStatus string `json:"verification_status"`
Disposition string `json:"disposition"`
SupervisionMode SupervisionMode `json:"supervision_mode"`
Repository string `json:"repository"`
RepositoryID string `json:"repository_id,omitempty"`
WorktreeID string `json:"worktree_id,omitempty"`
ControllerRoot string `json:"controller_root"`
HelperPath string `json:"helper_path"`
Feature string `json:"feature"`
SourcePlan string `json:"source_plan"`
SourcePlanSHA256 string `json:"source_plan_sha256"`
Artifact string `json:"artifact"`
ArtifactPath string `json:"artifact_path"`
DocumentSHA256 string `json:"document_sha256"`
Shell BootstrapShell `json:"shell"`
Argv []string `json:"argv"`
PlanningEnvelope string `json:"planning_envelope"`
}

func normalizedPlanningDocument(document []byte) ([]byte, error) {
document = normalizePlanningTransportBytes(document)
if reason := validPlanningBody(string(document)); reason != "" {
return nil, fmt.Errorf("planning document is invalid: %s", reason)
}
value := append([]byte(nil), document...)
if len(value) == 0 || value[len(value)-1] != '\n' {
value = append(value, '\n')
}
return value, nil
}

func bootstrapFeatureDisposition(repo string, workspace WorkspaceContext, feature string) (string, error) {
directory := workspace.FeatureDir(feature)
info, err := os.Lstat(directory)
if os.IsNotExist(err) {
return "CREATE_CANDIDATE", nil
}
if err != nil {
return "", err
}
if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
return "", fmt.Errorf("feature %s has conflicting planning state; run recovery-status before bootstrapping", feature)
}
statePath, stateErr := deliveryStatePath(repo, feature)
if stateErr != nil {
return "", stateErr
}
for _, path := range []string{
statePath,
filepath.Join(directory, "plan.lock.json"),
filepath.Join(directory, "pr.md"),
filepath.Join(directory, "approval.md"),
filepath.Join(directory, "autonomy.md"),
} {
if fileExists(path) {
return "", fmt.Errorf("feature %s already carries managed authority; use flow next --feature %s", feature, feature)
}
}
entries, err := os.ReadDir(directory)
if err != nil {
return "", err
}
for _, entry := range entries {
if entry.IsDir() || !planningArtifacts[entry.Name()] {
return "", fmt.Errorf("feature %s has conflicting planning state; run recovery-status before bootstrapping", feature)
}
}
if fileExists(filepath.Join(directory, "plan.md")) {
if _, err := CheckPlan(filepath.Join(directory, "plan.md")); err != nil {
return "", fmt.Errorf("feature %s has an invalid saved plan; run recovery-status before bootstrapping: %w", feature, err)
}
}
return "RESUME_CANDIDATE", nil
}

func bootstrapProgram(workspace WorkspaceContext, shell BootstrapShell) string {
if workspace.Mode == SupervisionDetached {
return workspace.HelperPath()
}
return workspace.LauncherPath(shell == BootstrapShellPowerShell)
}

func planningArgv(program, repo, feature, artifact, sourcePlan, sourceSHA string) []string {
return []string{
program, "planning-write",
"--repo", repo,
"--feature", feature,
"--artifact", artifact,
"--source-plan", sourcePlan,
"--source-plan-sha256", sourceSHA,
}
}

func posixPlanningEnvelopeFor(argv []string, document []byte) string {
words := make([]string, len(argv))
for index, word := range argv {
words[index] = posixPlanningWord(word)
}
delimiter := "BOATSTACK_PLAN_" + strings.ToUpper(SHA256Bytes(document)[:16])
return strings.Join(words, " ") + " <<'" + delimiter + "'\n" + string(document) + delimiter + "\n"
}

func powerShellPlanningWord(value string) string {
return "'" + strings.ReplaceAll(value, "'", "''") + "'"
}

func powerShellPlanningEnvelopeFor(argv []string, document []byte) (string, error) {
for _, line := range strings.Split(strings.ReplaceAll(string(document), "\r\n", "\n"), "\n") {
if strings.HasPrefix(line, "'@") {
return "", fmt.Errorf("PowerShell cannot represent a document line beginning with '@; use --shell posix with Git Bash")
}
}
words := make([]string, len(argv))
for index, word := range argv {
if strings.Contains(word, "'") {
return "", fmt.Errorf("PowerShell cannot safely bind an argument containing a single quote; use --shell posix with Git Bash")
}
words[index] = powerShellPlanningWord(word)
}
return "& {\n" + powerShellPlanningEncodingLine + "\n@'\n" + string(document) + "'@ | & " + strings.Join(words, " ") + "\n" + powerShellPlanningExitLine + "\n}\n", nil
}

// ResolvePlanningBootstrap is pure with respect to repository and controller
// state: it validates current evidence and returns bytes to execute, but writes
// nothing. The later planning-write rechecks the source-plan digest before its
// atomic first write.
// control-law: bootstrap-command-authority-is-workspace-bound
func ResolvePlanningBootstrap(options BootstrapOptions) (BootstrapPrescription, error) {
if !featureSlugPattern.MatchString(options.Feature) {
return BootstrapPrescription{}, fmt.Errorf("feature must be a lowercase kebab-case slug")
}
if !planningArtifacts[options.Artifact] {
return BootstrapPrescription{}, fmt.Errorf("unsupported planning artifact %q; use one of: %s", options.Artifact, strings.Join(planningArtifactNames(), ", "))
}
if options.Shell != BootstrapShellPOSIX && options.Shell != BootstrapShellPowerShell {
return BootstrapPrescription{}, fmt.Errorf("shell must be posix or powershell")
}
document, err := normalizedPlanningDocument(options.Document)
if err != nil {
return BootstrapPrescription{}, err
}
repo, err := ResolveRepository(options.Repo)
if err != nil {
return BootstrapPrescription{}, err
}
if err := CheckInstallationHealth(repo); err != nil {
return BootstrapPrescription{}, fmt.Errorf("bootstrap requires a healthy Boatstack installation: %w", DoctorRepairHint(err))
}
workspace, err := ResolveWorkspaceContext(repo)
if err != nil {
return BootstrapPrescription{}, err
}
sourcePlan, err := DiscoverSourcePlan(repo, options.SourcePlan)
if err != nil {
return BootstrapPrescription{}, err
}
sourceAbsolute := filepath.Join(repo, filepath.FromSlash(sourcePlan))
if err := rejectSymlinkComponents(repo, sourceAbsolute); err != nil {
return BootstrapPrescription{}, fmt.Errorf("source plan must be a regular in-repository file without symlink indirection: %w", err)
}
if pathWithin(workspace.FeatureDir(options.Feature), sourceAbsolute) {
return BootstrapPrescription{}, fmt.Errorf("source plan must remain outside the generated feature package")
}
sourceSHA, err := SHA256File(sourceAbsolute)
if err != nil {
return BootstrapPrescription{}, err
}
disposition, err := bootstrapFeatureDisposition(repo, workspace, options.Feature)
if err != nil {
return BootstrapPrescription{}, err
}
program := bootstrapProgram(workspace, options.Shell)
argv := planningArgv(program, repo, options.Feature, options.Artifact, sourcePlan, sourceSHA)
if options.Shell == BootstrapShellPOSIX {
// Git Bash accepts Windows drive paths in slash form. Keep the typed argv
// identical to the bytes rendered for that shell.
argv[0] = filepath.ToSlash(argv[0])
argv[3] = filepath.ToSlash(argv[3])
}
var envelope string
if options.Shell == BootstrapShellPowerShell {
envelope, err = powerShellPlanningEnvelopeFor(argv, document)
} else {
envelope = posixPlanningEnvelopeFor(argv, document)
}
if err != nil {
return BootstrapPrescription{}, err
}
return BootstrapPrescription{
SchemaVersion: bootstrapPrescriptionSchemaVersion, VerificationStatus: "VERIFIED",
Disposition: disposition, SupervisionMode: workspace.Mode,
Repository: repo, RepositoryID: workspace.RepoID, WorktreeID: workspace.WorktreeID,
ControllerRoot: workspace.ExportRoot(), HelperPath: program,
Feature: options.Feature, SourcePlan: sourcePlan, SourcePlanSHA256: sourceSHA,
Artifact: options.Artifact, ArtifactPath: filepath.Join(workspace.FeatureDir(options.Feature), options.Artifact),
DocumentSHA256: SHA256Bytes(document), Shell: options.Shell, Argv: argv,
PlanningEnvelope: envelope,
}, nil
}
Loading