From d1e00e796ac36c4fc130976eb6596eb28c78f0f4 Mon Sep 17 00:00:00 2001 From: schickling-assistant <261620128+schickling-assistant@users.noreply.github.com> Date: Tue, 10 Nov 2026 20:24:37 +0100 Subject: [PATCH] Mirror ten Fractal R2 paths with their immutable fixture input --- flake.lock | 8 +-- flake.nix | 2 +- scripts/evidence-sign | 11 +++- scripts/evidence-sign-hardening-test | 3 +- scripts/evidence-verify-test | 6 +- scripts/evidence-wrap-test | 64 +++++++++++++++++++ scripts/lane-definition-closure | 2 +- scripts/st-test | 34 ++++++++++ .../fractal/.st/evidence-signed-only.json | 3 +- .../fractal/.st/evidence-skip-policy.json | 10 ++- .../fixtures/fractal/.st/required-lanes.json | 13 +++- .../fractal/.st/rust-test-inputs.json | 6 +- 12 files changed, 145 insertions(+), 17 deletions(-) diff --git a/flake.lock b/flake.lock index 8c1e900..4c48737 100644 --- a/flake.lock +++ b/flake.lock @@ -42,17 +42,17 @@ "fractal-fixture": { "flake": false, "locked": { - "lastModified": 1794265163, - "narHash": "sha256-pGJRiUHcongVBrYEWT2w3TZTS8UdcZJjHRJvTDhrpYg=", + "lastModified": 1794333982, + "narHash": "sha256-EYnLgrxDCQXThOybpPxr2okTy7BpcHkzjLvBsaAZ+vM=", "owner": "compoundingtech", "repo": "fractal", - "rev": "e2721e52463428b4ff655addee129cecd2839763", + "rev": "787e8ed592dff34d2bdc2a2b0b584482da567e7f", "type": "github" }, "original": { "owner": "compoundingtech", "repo": "fractal", - "rev": "e2721e52463428b4ff655addee129cecd2839763", + "rev": "787e8ed592dff34d2bdc2a2b0b584482da567e7f", "type": "github" } }, diff --git a/flake.nix b/flake.nix index 302f540..6c39b45 100644 --- a/flake.nix +++ b/flake.nix @@ -7,7 +7,7 @@ inputs.effect-utils.inputs.nixpkgs.url = "github:NixOS/nixpkgs/4ba99f3209788ed04f01af38f77ddb803ad6ec63"; inputs.effect-utils.inputs.tsgo.inputs.nixpkgsUnstable.url = "github:NixOS/nixpkgs/44a91898084f46797b5fac650c7e8c9ac38c43d4"; inputs.fractal-fixture = { - url = "github:compoundingtech/fractal/e2721e52463428b4ff655addee129cecd2839763"; + url = "github:compoundingtech/fractal/787e8ed592dff34d2bdc2a2b0b584482da567e7f"; flake = false; }; inputs.python-oracle = { diff --git a/scripts/evidence-sign b/scripts/evidence-sign index 288b9c2..ea0ab64 100755 --- a/scripts/evidence-sign +++ b/scripts/evidence-sign @@ -198,7 +198,7 @@ def child_environment(extra=None, environ=None): environ = os.environ if environ is None else environ child = {name: environ[name] for name in PASSED_ENVIRONMENT if name in environ} child.update(PATH=TOOLS['PATH'], LC_ALL='C', GIT_NO_REPLACE_OBJECTS='1', GIT_TERMINAL_PROMPT='0', - GIT_CONFIG_NOSYSTEM='1', GIT_CONFIG_GLOBAL='/dev/null') + GIT_CONFIG_NOSYSTEM='1', GIT_CONFIG_GLOBAL='/dev/null', GIT_NO_LAZY_FETCH='1') child.update(extra or {}) return child @@ -215,6 +215,14 @@ def git(repo, *args, stdin=None, env=None): return done.stdout +def require_complete_commit(repo, commit): + """The caller hydrates its checkout; the signer never contacts a promisor remote.""" + missing = sum(line.startswith('?') for line in + git(repo, 'rev-list', '--objects', '--missing=print', commit).decode().splitlines()) + if missing: + raise Refused(f'{missing} missing objects before signing; hydrate the source repository in the caller first') + + def check_nix(request): """A `nix` request is `nix build .#checks..` and says what that build produced. The signer cannot run Nix or see the store, so it holds the claim to its shape: store paths, a NAR hash, the signatures @@ -303,6 +311,7 @@ def main(): checkout = Path.cwd() commit = request['commit'] + require_complete_commit(checkout, commit) if git(checkout, 'cat-file', '-t', commit).decode().strip() != 'commit': raise Refused('commit is not a commit in the caller\'s checkout') tree = git(checkout, 'rev-parse', f'{commit}^{{tree}}').decode().strip() diff --git a/scripts/evidence-sign-hardening-test b/scripts/evidence-sign-hardening-test index 6889e6c..7da656f 100755 --- a/scripts/evidence-sign-hardening-test +++ b/scripts/evidence-sign-hardening-test @@ -125,9 +125,10 @@ hostile = {'LD_LIBRARY_PATH': '/evil', 'LD_PRELOAD': '/evil.so', 'SSH_AUTH_SOCK' 'GIT_SSH_COMMAND': 'evil', 'PYTHONUNBUFFERED': '1', 'PERL5OPT': '-Mevil', 'HTTPS_PROXY': 'http://evil', 'PATH': '/evil/bin', 'LC_ALL': 'evil', 'HOME': '/srv/example-home', 'TMPDIR': '/var/tmp/x', 'FOO': 'bar'} child = signer.child_environment(environ=hostile) -assert set(child) == {'HOME', 'TMPDIR', 'PATH', 'LC_ALL', 'GIT_NO_REPLACE_OBJECTS', 'GIT_TERMINAL_PROMPT', 'GIT_CONFIG_NOSYSTEM', 'GIT_CONFIG_GLOBAL'}, sorted(child) +assert set(child) == {'HOME', 'TMPDIR', 'PATH', 'LC_ALL', 'GIT_NO_REPLACE_OBJECTS', 'GIT_TERMINAL_PROMPT', 'GIT_CONFIG_NOSYSTEM', 'GIT_CONFIG_GLOBAL', 'GIT_NO_LAZY_FETCH'}, sorted(child) assert child['PATH'] == '/usr/bin:/bin' and child['LC_ALL'] == 'C' and child['HOME'] == '/srv/example-home' and child['TMPDIR'] == '/var/tmp/x' assert child['GIT_CONFIG_GLOBAL'] == '/dev/null' and child['GIT_CONFIG_NOSYSTEM'] == '1' +assert child['GIT_NO_LAZY_FETCH'] == '1' extra = signer.child_environment({'GIT_AUTHOR_NAME': 'Evidence signer'}, environ=hostile) assert extra['GIT_AUTHOR_NAME'] == 'Evidence signer', 'settings the signer supplies itself are kept' print('passed: git and ssh-keygen get PATH, LC_ALL, HOME, TMPDIR and the signer\'s own git settings; loader, agent, proxy and GIT_* variables are dropped') diff --git a/scripts/evidence-verify-test b/scripts/evidence-verify-test index 08d11cc..6b9d6c5 100755 --- a/scripts/evidence-verify-test +++ b/scripts/evidence-verify-test @@ -1460,12 +1460,14 @@ class CommittedPolicy(unittest.TestCase): '.cargo', '.config/nextest.toml', '.github', 'Cargo.lock', 'Cargo.toml', 'claude-channel', 'clients', 'components', 'crates', 'docs/st3', 'docs/vrs', 'evals', 'examples', 'fixtures', 'flake.lock', 'flake.nix', 'nix', 'plugins', 'scripts', 'apps/fractal-web/src/terminal/assets/JetBrainsMonoNerdFontMono-Regular.ttf', # read at run time by a test in crates/fractal-daemon/src/attach_image.rs + 'packages/fractal/fixtures', # included by crates/fractal/tests/validate.rs 'slo', 'templates', 'vendor', 'wit']), 'typescript-client': sorted([ 'apps/fractal-web/package.json', 'packages/fractal-sdk/package.json', 'apps/fractal-web/src/folders/core.mts', 'apps/ios', # core.mts: found by the lane trace (docs: Lane definitions) 'clients/typescript', 'crates/fractal-client-codegen/templates/declarations.ts.in', 'crates/fractal-tui/tests/fixtures/quality-hour.json', 'docs/st3/client-v0/fixtures', - 'fixtures/clients', 'flake.lock', 'flake.nix', 'package.json', 'packages/fractal-ui/package.json', + 'docs/site', # the fractal package's docs test renders every path-commented TS sample there + 'fixtures/clients', 'flake.lock', 'flake.nix', 'package.json', 'packages/fractal', 'packages/fractal-ui/package.json', 'pnpm-lock.yaml', 'pnpm-workspace.yaml', 'scripts/ci-typescript-client']), 'fractal-web': sorted([ # The web lane detector's paths (scripts/ci-fractal-web-changes, PATTERNS and KIT_PATTERNS, as files that exist)... '.buckconfig', '.buckconfig.genie.ts', '.buckroot', '.github/repo-settings.json', '.github/repo-settings.json.genie.ts', @@ -1473,7 +1475,7 @@ class CommittedPolicy(unittest.TestCase): '.github/workflows/main-upkeep.yml', '.github/workflows/main-upkeep.yml.genie.ts', '.github/workflows/workspace-ci.ts', 'BUCK', 'BUCK.genie.ts', 'apps/fractal-web', 'buck2', 'clients/typescript/fractal-client', 'clients/typescript/fractal-scenarios', 'clients/typescript/fractal-views', 'docs/st3/client-v0/schemas', 'fixtures', 'flake.lock', 'flake.nix', 'megarepo.kdl', - 'megarepo.lock', 'nix/fractal-web.nix', 'nix/web-shell-inputs.json', 'nix/web-shell.nix', 'package.json', 'package.json.genie.ts', 'packages/fractal-sdk', 'packages/fractal-ui', + 'megarepo.lock', 'nix/fractal-web-publisher.nix', 'nix/fractal-web.nix', 'nix/web-shell-inputs.json', 'nix/web-shell.nix', 'package.json', 'package.json.genie.ts', 'packages/fractal-sdk', 'packages/fractal-ui', 'pnpm-lock.yaml', 'pnpm-workspace.yaml', 'pnpm-workspace.yaml.genie.ts', 'scripts/ci-fractal-web', 'scripts/ci-fractal-web-changes', 'scripts/ci-fractal-web-gate', 'scripts/ci-fractal-web-kit-graph', 'scripts/ci-fractal-web-licenses', 'scripts/ci-fractal-web-licenses-test', 'scripts/ci-fractal-web-test', 'scripts/ci-typescript-client', 'scripts/ci-web-shell', diff --git a/scripts/evidence-wrap-test b/scripts/evidence-wrap-test index 603ae3a..7118478 100755 --- a/scripts/evidence-wrap-test +++ b/scripts/evidence-wrap-test @@ -682,6 +682,70 @@ os.execv({real_git!r}, ['git', *args]) assert refused.returncode == 1 and b'did not verify locally' in refused.stderr, refused.stderr print('passed: a per-crate record is verified as that crate\'s: a failed record of another crate does not decide it, its own unverified record still blocks a push') + # A local upload-pack disables lazy fetching. HEAD's checkout is not enough: + # the scratch fetch also needs blobs reachable only through old commits. + source = tmp / 'partial-source' + git(tmp, 'init', '-q', str(source)) + (source / 'flake.lock').write_text('{"nodes":{}}\n') + (source / 'historical').write_text('only in history\n') + git(source, 'add', '.') + git(source, 'commit', '-q', '-m', 'historical blob') + git(source, 'rm', 'historical') + (source / '.st').mkdir() + (source / '.st/evidence-keys').write_text(signers.read_text()) + git(source, 'add', '.') + git(source, 'commit', '-q', '-m', 'remove historical blob') + origin = tmp / 'partial-origin.git' + git(tmp, 'clone', '-q', '--bare', str(source), str(origin)) + git(origin, 'config', 'uploadpack.allowFilter', 'true') + + def partial_clone(name): + checkout = tmp / name + git(tmp, 'clone', '-q', '--filter=blob:none', origin.as_uri(), str(checkout)) + assert git(checkout, 'config', '--get', 'remote.origin.promisor') == 'true' + git(checkout, 'update-ref', 'refs/remotes/origin/main', 'HEAD') + assert any(line.startswith('?') for line in + git(checkout, 'rev-list', '--objects', '--missing=print', 'HEAD').splitlines()) + return checkout + + partial = partial_clone('partial-checkout') + unavailable = partial_clone('unavailable-checkout') + partial_home = tmp / 'partial-home' + (partial_home / '.config/st').mkdir(parents=True) + (partial_home / '.config/st/evidence-sign.json').write_text(json.dumps( + {'signer': 'fleet/dev', 'ssh_key': str(key), 'scratch_repo': str(tmp / 'partial-scratch.git'), + 'test_clock': True, 'path': tool_path()})) + + def partial_wrap(checkout): + return run([sys.executable, str(WRAP), '--check', 'linux-gate', + '--toolchain-sha256', toolchain, '--signer', signer, + '--change-id', 'github:example/repo#5', '--', 'true'], + checkout, partial_home) + + caller = runpy.run_path(str(ROOT / 'scripts/st-test')) + caller['hydrate_commit'](partial, git(partial, 'rev-parse', 'HEAD')) + signed = partial_wrap(partial) + assert signed.returncode == 0, signed.stderr.decode() + assert not any(line.startswith('?') for line in + git(partial, 'rev-list', '--objects', '--missing=print', 'HEAD').splitlines()) + signed_result = json.loads(signed.stderr.decode()[signed.stderr.decode().index('{'):]) + git(partial, '-c', 'gpg.format=ssh', '-c', f'gpg.ssh.allowedSignersFile={signers}', + 'verify-commit', signed_result['record']) + print('passed: caller hydrates blob:none historical blobs before signing with a verified signature') + + marker = tmp / 'transport-ran' + git(unavailable, 'remote', 'set-url', 'origin', 'ssh://invalid.example/repository') + git(unavailable, 'config', 'core.sshCommand', f'touch {marker}') + request = dict(good, commit=git(unavailable, 'rev-parse', 'HEAD')) + refused = subprocess.run([sys.executable, str(SIGN)], cwd=unavailable, + input=json.dumps(request).encode(), capture_output=True, + env=dict(ENV, HOME=str(partial_home), EVIDENCE_SIGN_NOW=DATE)) + assert refused.returncode != 0, refused.stderr.decode() + assert b'missing objects before signing; hydrate the source repository in the caller first' in refused.stderr, refused.stderr.decode() + assert not marker.exists(), 'the signer must not run a transport' + assert not git(unavailable, 'for-each-ref', 'refs/tags/evidence', 'refs/notes/evidence') + print('passed: signer refuses missing partial-clone objects without running a transport or publishing evidence') + if write_fixture: write_fixture.mkdir(parents=True, exist_ok=True) fixture_home = tmp / 'fixture-home' diff --git a/scripts/lane-definition-closure b/scripts/lane-definition-closure index 492b374..5a84462 100755 --- a/scripts/lane-definition-closure +++ b/scripts/lane-definition-closure @@ -135,7 +135,7 @@ EXTRA = { # kit-play-shards.mjs to test it). A package.json whose script a lane's pnpm call runs is in the closure (its `scripts` entry is the # command). Nothing outside SUBJECT is assumed to be the subject. SUBJECT = ('Cargo.toml', 'Cargo.lock', 'src', 'tests', 'crates', 'components', 'examples', 'wit', 'vendor', - 'apps/fractal-web', 'apps/ios', 'clients/typescript', 'packages/fractal-sdk', 'packages/fractal-ui') + 'apps/fractal-web', 'apps/ios', 'clients/typescript', 'packages/fractal', 'packages/fractal-sdk', 'packages/fractal-ui') HARNESS = ('apps/fractal-web/scripts/ci', 'packages/fractal-ui/scripts') HARNESS_LANES = {'fractal-web'} # Where the lanes' own code lives. A file elsewhere that the code names (a workflow, a doc, a fixture, a baseline) is read as data: diff --git a/scripts/st-test b/scripts/st-test index 087ae7d..feb0d16 100755 --- a/scripts/st-test +++ b/scripts/st-test @@ -362,6 +362,38 @@ def git(*args, cwd=None, env=None, check=True, stdin=None): return done +def hydrate_commit(directory, commit): + """Unprivileged preparation: local upload-pack cannot lazy-fetch historical blobs.""" + def missing_objects(): + return [line[1:] for line in + git('rev-list', '--objects', '--missing=print', commit, cwd=directory, + env={'GIT_NO_LAZY_FETCH': '1'}).stdout.decode().splitlines() + if line.startswith('?')] + + missing = missing_objects() + if not missing: + return + remotes = [match.group(1) for line in git('config', '--list', cwd=directory).stdout.decode().splitlines() + if (match := re.fullmatch(r'remote\.(.+)\.promisor=true', line))] + if not remotes: + raise Failure(f'cannot hydrate {len(missing)} missing objects before signing: no promisor remote') + while missing: + before = set(missing) + for offset in range(0, len(missing), 200): + batch = missing[offset:offset + 200] + try: + git('-c', 'maintenance.auto=false', '-c', 'gc.auto=0', + '-c', 'fetch.negotiationAlgorithm=noop', 'fetch', '--quiet', + '--no-tags', '--no-write-fetch-head', '--no-auto-maintenance', + '--no-recurse-submodules', '--filter=blob:none', '--stdin', remotes[0], + cwd=directory, stdin=(''.join(oid + '\n' for oid in batch)).encode()) + except Failure as error: + raise Failure(f'cannot hydrate missing objects before signing: {error}') from error + missing = missing_objects() + if missing and set(missing) >= before: + raise Failure(f'cannot hydrate {len(missing)} missing objects before signing: fetch made no progress') + + # arn:lmig:smalltalk:2026-10-27-dot-fractal-config — embedded for base runners copied alone. def config_path(base, name): for directory in ('.fractal', '.st'): @@ -3755,6 +3787,8 @@ def run_main(argv, cleanup): round_number, round_started = top_up, time.monotonic() # 0: the run itself; then the top-ups, including a handed-over round target, target_lock = None, None while True: + # Use the caller's normal git context, never the key-bearing signer's. + hydrate_commit(directory, commit) tools = {} for name in BASE_SCRIPTS: # from the base as it is now, after any wait for a tree path = Path(scratch) / name diff --git a/tests/fixtures/fractal/.st/evidence-signed-only.json b/tests/fixtures/fractal/.st/evidence-signed-only.json index fbce342..f6f32f5 100644 --- a/tests/fixtures/fractal/.st/evidence-signed-only.json +++ b/tests/fixtures/fractal/.st/evidence-signed-only.json @@ -2,7 +2,6 @@ "about": "Pull requests opened by these GitHub accounts are pre-signed (scripts/st-test --all --push origin) and never run the heavy checks in CI: an enforced check with no valid signature for its tree fails with what to do instead (scripts/ci-evidence-gate, refusal). Read from BASE, so a pull request cannot take itself off the list or enable queue reuse. merge_group is off until conditional post-merge hosted proof is deployed; same-tree permits full-tier evidence for the exact queue tree, with the BASE skip policy's keys, commands, inputs and lanes. Other merge groups run hosted CI; invalid evidence never refuses a merge group.", "merge_group": "off", "openers": [ - "myobie", - "schickling-assistant" + "myobie" ] } diff --git a/tests/fixtures/fractal/.st/evidence-skip-policy.json b/tests/fixtures/fractal/.st/evidence-skip-policy.json index 8da8120..1b5e25c 100644 --- a/tests/fixtures/fractal/.st/evidence-skip-policy.json +++ b/tests/fixtures/fractal/.st/evidence-skip-policy.json @@ -30,6 +30,7 @@ "flake.lock", "flake.nix", "nix", + "packages/fractal/fixtures", "plugins", "scripts", "slo", @@ -207,6 +208,7 @@ "fixtures", "flake.lock", "flake.nix", + "packages/fractal/fixtures", "plugins/claude", "scripts/build-stamp-watch-test", "scripts/check-fractal-boundary", @@ -262,7 +264,7 @@ "scripts/script-test-supervisor", "scripts/script_process.py", "scripts/sqlite_large_values.py", - "slo/targets.toml", + "slo/objectives.kdl", "vendor/libghostty-vt-sys", "wit" ], @@ -443,6 +445,7 @@ "fixtures", "flake.lock", "flake.nix", + "packages/fractal/fixtures", "plugins/claude", "scripts/build-stamp-watch-test", "scripts/check-fractal-boundary", @@ -497,7 +500,7 @@ "scripts/script-test-supervisor", "scripts/script_process.py", "scripts/sqlite_large_values.py", - "slo/targets.toml", + "slo/objectives.kdl", "vendor/libghostty-vt-sys", "wit" ], @@ -607,11 +610,13 @@ "clients/typescript", "crates/fractal-client-codegen/templates/declarations.ts.in", "crates/fractal-tui/tests/fixtures/quality-hour.json", + "docs/site", "docs/st3/client-v0/fixtures", "fixtures/clients", "flake.lock", "flake.nix", "package.json", + "packages/fractal", "packages/fractal-ui/package.json", "pnpm-lock.yaml", "pnpm-workspace.yaml", @@ -1041,6 +1046,7 @@ "flake.nix", "megarepo.kdl", "megarepo.lock", + "nix/fractal-web-publisher.nix", "nix/fractal-web.nix", "nix/web-shell-inputs.json", "nix/web-shell.nix", diff --git a/tests/fixtures/fractal/.st/required-lanes.json b/tests/fixtures/fractal/.st/required-lanes.json index c30dd4a..f378489 100644 --- a/tests/fixtures/fractal/.st/required-lanes.json +++ b/tests/fixtures/fractal/.st/required-lanes.json @@ -68,6 +68,7 @@ "flake.nix", "megarepo.kdl", "megarepo.lock", + "nix/fractal-web-publisher.nix", "nix/fractal-web.nix", "nix/web-shell-inputs.json", "nix/web-shell.nix", @@ -127,6 +128,7 @@ "flake.lock", "flake.nix", "package.json", + "packages/fractal/package.json", "packages/fractal-sdk/package.json", "packages/fractal-ui/package.json", "pnpm-lock.yaml", @@ -278,6 +280,7 @@ "flake.lock", "flake.nix", "nix", + "packages/fractal/fixtures", "plugins", "scripts", "slo", @@ -377,6 +380,7 @@ "flake.lock", "flake.nix", "nix", + "packages/fractal/fixtures", "plugins", "scripts", "slo", @@ -414,6 +418,7 @@ "nix/typescript-client.nix", "nix/web-shell.nix", "package.json", + "packages/fractal/package.json", "pnpm-lock.yaml", "pnpm-source-inputs.json", "pnpm-workspace.yaml", @@ -429,11 +434,13 @@ "clients/typescript", "crates/fractal-client-codegen/templates/declarations.ts.in", "crates/fractal-tui/tests/fixtures/quality-hour.json", + "docs/site", "docs/st3/client-v0/fixtures", "fixtures/clients", "flake.lock", "flake.nix", "package.json", + "packages/fractal", "packages/fractal-ui/package.json", "pnpm-lock.yaml", "pnpm-workspace.yaml", @@ -575,6 +582,7 @@ "fixtures", "flake.lock", "flake.nix", + "packages/fractal/fixtures", "plugins/claude", "scripts/build-stamp-watch-test", "scripts/check-fractal-boundary", @@ -630,7 +638,7 @@ "scripts/script-test-supervisor", "scripts/script_process.py", "scripts/sqlite_large_values.py", - "slo/targets.toml", + "slo/objectives.kdl", "vendor/libghostty-vt-sys", "wit" ] @@ -782,6 +790,7 @@ "fixtures", "flake.lock", "flake.nix", + "packages/fractal/fixtures", "plugins/claude", "scripts/build-stamp-watch-test", "scripts/check-fractal-boundary", @@ -836,7 +845,7 @@ "scripts/script-test-supervisor", "scripts/script_process.py", "scripts/sqlite_large_values.py", - "slo/targets.toml", + "slo/objectives.kdl", "vendor/libghostty-vt-sys", "wit" ] diff --git a/tests/fixtures/fractal/.st/rust-test-inputs.json b/tests/fixtures/fractal/.st/rust-test-inputs.json index 43b975c..04e7003 100644 --- a/tests/fixtures/fractal/.st/rust-test-inputs.json +++ b/tests/fixtures/fractal/.st/rust-test-inputs.json @@ -43,6 +43,9 @@ "examples/demos", "examples/fractal", "fixtures", + "packages/fractal/fixtures/daily-review.kdl", + "packages/fractal/fixtures/pr-landing.kdl", + "packages/fractal/fixtures/upstream-repin.kdl", "scripts", "slo" ] @@ -234,7 +237,8 @@ "crates/fractal-daemon/hooks/claude-observe.sh", "crates/fractal-daemon/hooks/claude-statusline.sh", "crates/fractal-daemon/hooks/omp-channel.ts", - "crates/fractal-daemon/hooks/pi-channel.ts" + "crates/fractal-daemon/hooks/pi-channel.ts", + "slo/objectives.kdl" ] }, "fractal-migrate": {