diff --git a/README.md b/README.md index 2026d53..b7dbfcd 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # Factory -Revision: 0.1.18 · Updated: 2026-10-19 +Revision: 0.1.20 · Updated: 2026-10-22 A practical method for building and improving applications with coding agents. @@ -28,6 +28,23 @@ owns the application's setup and work. The AgentOps conversation in the Factory repository develops Factory itself; it is not a central queue for every project. Share a relevant finding between projects when useful, with an explicit handoff. +## Use Factory from your own AI agent + +Give your everyday AI agent the [Factory entry skill](agent-ops/factory-entry/SKILL.md). +It tells that agent where each request belongs: + +- Work on Factory's method goes to the Factory repository's AgentOps. +- Work on an application goes to that application's own AgentOps. +- A request spanning several applications becomes one bounded handoff per lead, + and your agent coordinates what they share. +- "Help me make this machine a Factory host and explain its role" loads + Foundation and its existing host setup guide. + +Your agent carries the request, candidate, authority and holds between leads and +follows up. It does not become a lead or gain approval rights. Install it with the +[setup guide](foundation/factory-foundation/references/setup.md#the-owners-own-ai-agent); +installed, discovered and actually used are separate checks. + ## Why I built this I’m building Factory to give software work with agents a modular structure, from diff --git a/VERSION b/VERSION index d8a023e..baa9837 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1.19 +0.1.20 diff --git a/agent-ops/factory-entry/SKILL.md b/agent-ops/factory-entry/SKILL.md new file mode 100644 index 0000000..b8098e7 --- /dev/null +++ b/agent-ops/factory-entry/SKILL.md @@ -0,0 +1,89 @@ +--- +name: factory-entry +description: Use when an owner's everyday AI agent is asked to use Factory, start or follow up work, route a request to the right project, or make a machine a Factory host and explain its role. Routes to the existing project lead; it is not any project's lead. +license: MIT +metadata: + version: "0.1.20" + updated: "2026-10-22" +--- + +# Factory Entry + +You are the owner's own AI agent. You coordinate the owner's goals across their +projects. You do not become a project's lead, a host administrator or an approver. + +Factory's roles, each a separate canonical skill: + +- `factory-agent-ops`: the lead in one project's pinned AgentOps thread. It owns + that project's priorities, workers, review and acceptance within its mandate. +- `factory-foundation`: prepares and repairs host and project conditions, and holds + the reusable setup method, including host continuity. +- ADLC skills (`factory-triage`, `factory-spec`, `factory-implement`, + `factory-review`, `factory-security`, `factory-evaluate`): run the concrete task + under a lead. + +Load a peer skill only when the request needs it. Resolve it by name in your native +skill catalogue, or at its actual installed path. If it is not installed, you may +read it explicitly from a reviewed Factory checkout. That is explicit loading, not +native discovery, and a path you cannot read grants nothing. + +## Route the request + +- **Factory method development** goes to the Factory repository's existing AgentOps. +- **Work on an application** goes to that application's existing AgentOps. +- **Several applications** need one bounded handoff per existing lead. You + coordinate any shared dependency and ordering between them. +- **Missing or stale recipient:** reconcile the actual native project, workspace and + thread identity with the retained adoption record. A familiar thread title is not + authority. If no lead exists, ask the owner, or ask Foundation to establish one. + Never fall back to Factory's own lead, and never start a second lead. + +A lead that could do the work is not therefore the right entry. Within that lead, +an idea or research request ends in a recommendation, not admitted work. An +accepted task or backlog proceeds under existing authority. A setup or readiness +gap goes to Foundation for the affected capability only, not a rerun of all +setup. A follow-up returns to the same lead, and a candidate for human review goes +to the owner. + +## Hand off and follow through + +Each handoff states the following separately: + +- the request and its intended outcome; +- the current candidate and evidence; +- admission authority and delivery authority, with any scoped delegation; +- constraints and current holds; +- the next action and who holds it. + +Keep these states apart: queued, consumed or acknowledged, actually started, +blocked, and delivered. A sent message is not started work. Read the recipient's +actual native state before any retry, correction or new assignment. Do not +interrupt an active worker or add a competing writer. + +The owner's two decisions stay with the owner unless they delegated them for a +defined scope: admitting work, and accepting delivery before merge or release. +Green CI or a positive review is not merge permission, and silence is not +approval. A changed candidate needs its decision checked again, and branch +protections still apply. Bring a genuinely missing owner decision forward plainly, +with the candidate and your recommendation, and keep following it up. + +After compaction, a restart or a new session, reread this skill, the owner's +current request, the retained record, recorded decisions and holds, and each +recipient's native state before taking any effect. + +## Make this machine a Factory host + +For "help me make this machine a Factory host and explain its role", load +`factory-foundation` and follow its setup guide, including its section "Keep the +host ready after restart". Recover the intended host, execution account, harness, +role, clients, recovery holder and existing project leads from the adoption record +before asking anything. Ask only for choices that are genuinely missing. Apply only +setup that is already authorized and routine. Keep the owner's own account and +personal connectors separate from the execution account. + +Explain to the owner who plans and coordinates, who owns each project, who +executes, and who accepts delivery. Report configured, running, reachable, +resumed and demonstrated useful work as separate results. + +Whether this skill was requested, installed, discovered in your session, and +actually used are four separate claims. diff --git a/docs/architecture.md b/docs/architecture.md index 9adf02d..fe22bd2 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,12 +1,13 @@ # Responsibility and access -Revision: 0.1.19 · Updated: 2026-10-21 +Revision: 0.1.20 · Updated: 2026-10-22 Factory has three responsibilities around existing tools. It has no server, provider adapter, task database or application runtime of its own. | Responsibility | Owns | Does not establish | | --- | --- | --- | +| Owner's own AI agent (`factory-entry`) | Routing each request to the right existing project lead, bounded handoffs across projects, and following them up | A project's priorities, workers or acceptance; approval, merge or administrator rights | | Foundation | Project context, native setup, access choices and readiness checks; repair when these change | Product scope, continuous execution or readiness merely because files were installed | | Host coordinator (optional) | Host continuity, maintenance shared by several projects, and repair requests to Foundation, within an explicit owner mandate | Product priorities, worker assignments or acceptance in a project; administrator rights or personal connectors | | AgentOps | Research, priorities, assignments, worker model choices, reviewed skill/context improvements and acceptance within delegated authority | Administrator access, permission to bypass review or self-installed skills | diff --git a/docs/qualification.md b/docs/qualification.md index 9c985c8..a253cb4 100644 --- a/docs/qualification.md +++ b/docs/qualification.md @@ -1,6 +1,6 @@ # Qualification -Revision: 0.1.19 · Updated: 2026-10-21 +Revision: 0.1.20 · Updated: 2026-10-22 This record distinguishes source review, disposable exercises and observed native operation. A package release does not qualify another host, every future update @@ -9,7 +9,8 @@ remain with the installation; public issues track the remaining acceptance work. | Boundary | Observed evidence | Limit / next check | | --- | --- | --- | -| Package | Eight portable skills, metadata/link checks and staging tests | Mechanical checks do not establish agent decisions | +| Package | Portable skills (nine from 0.1.20, with `factory-entry`; eight before), metadata/link checks and staging tests | Mechanical checks do not establish agent decisions | +| Owner entry and all-skill staging (0.1.20 candidate) | 2026-10-22: [Factory #91](https://github.com/arcitai/factory/issues/91) source change, after an owner-side planning agent sent application work to the Factory repository's AgentOps as if it were Factory's general entry. That agent had no Factory skill enabled (owner report, not reproduced here). Adds the `factory-entry` skill, an `all` mode for the existing stage helper, and setup guidance for installing, discovering and using the entry. Portable skills and staging were chosen; plugin packaging stays optional. On the candidate, the package check passed with 9 skills and 0 errors, and 16 unit tests passed. All-set staging from the frozen source snapshot produced 9 skills and 28 manifest-bound files. Source candidate only; not delivered, released or installed | See the delivery PR for representative caller outcomes and independent review of the exact candidate. Installation, discovery and use in the owner's active caller have not been run; staging installed nothing | | Host continuity and maintenance (0.1.19 candidate) | 2026-10-21: [Factory #83](https://github.com/arcitai/factory/issues/83) source change, after an installation could look ready while operator apps did not start after login and no maintenance schedule was active (owner report, not reproduced here). Setup, adoption and architecture now describe the optional host coordinator, separate startup stages, one owned nightly maintenance opportunity, scoped holds and a same-host recovery copy. The existing owner coordinator reported one actual scheduled wake on 2026-10-21. On the frozen candidate: `scripts/check.py` passed with 8 skills and 0 errors, 11 unit tests passed, and staging Foundation produced its 9 portable files plus a manifest, which were not installed. In a fresh Sol 6.1 Max context, one application each of four synthetic cases matched the retained rubric (lead assessment, evidence kept privately): independent source work continued without a hold; a startup or wake was kept apart from recovery; unknown local account-tool exposure stayed unknown; and a real maintenance hold kept its timeouts and restored prior states. An earlier rehearsal was not run, because its brief prohibited the available reader; it is neither a pass nor a failure | These are bounded synthetic decisions, not live proof of boot, update, reconnect or isolation, and not a reliability result. That single wake is not proof of reboot, of operation with the Mac disconnected, or of full recovery. Controlled reboot, login and reconnect, safe update and continuation, and the host gates remain open live acceptance; this change marks none of them complete. Independent review of the exact final source is pending in the delivery PR | | Production artifact evidence (0.1.18 candidate) | Reconciled 17 October 2026 from product-lead handbacks ([Factory #76](https://github.com/arcitai/factory/issues/76); not reproduced here). OPD: the source expected eight embedded editor assets, but the installed executable held six and served editor-not-built. Its six tests substituted a `#[cfg(test)]` `tests::BUNDLE_FILES` list, while production uses `OUT_DIR/bundle.rs`, so those tests did not exercise production inclusion. The OPD lead's original handoff left a production smoke optional; the corrected R10 proof gate made it required. A same-source replacement was atomically adopted at 06:17:35Z (transferred receipt). AIOS: the contrast build installed at 06:18:55Z (298 files). Bridge: the owner passed queue history. The lead checked retained artifacts offline against the eight source-derived names: the old six-asset output was rejected for missing `editor.js` and `panel.js`, the fresh eight passed, and the R10 manifest and receipt agree. Six of the eight assets are runtime bytes; two are build provenance | Offline reuse of historical evidence; no binary was run, and this does not show that the method caused the recovery. Still open: the OPD native workflow and app-origin binding, and the AIOS native colours and opening. The missing Bridge toast is not a proven defect; no custom Undo is authorized. In one fresh-context rehearsal of five incident packets, every decision kept these limits: the incomplete package, the installed but unaccepted workflow and visual checks, the toast without an established contract, and the dropped accepted centre amendment. This tested decisions only, with no live product action and no claim of general reliability. Independent review of the exact candidate and its checks is recorded in the delivery PR | | Blocked-request follow-through (0.1.17 candidate) | 2026-10-15, after the held OPD request recorded below ([Factory #32](https://github.com/arcitai/factory/issues/32); coordinator native UI actions plus Factory native readback). Declining the held compound shell call failed it without running it, but the run stayed active and proposed a second shell change before the prepared text-only correction arrived. The coordinator then cancelled that held call: neither call ran, the run was interrupted and ended with nothing pending, and its pre-recovery checkpoint was kept. The OPD lead sent its accepted native-file-only instruction to the same worker at that checkpoint; the next run completed native file changes with no shell or approval items and nothing pending while still active | Operator-assisted progress after an interrupted blocked attempt, not an automatic wake, autonomous responder, uninterrupted run, completed diagnostic checks, review or product delivery, or OS isolation. The two settled requests are not to be re-disposed. The source guidance followed this event and did not cause it. Source checks and independent review are recorded in the delivery PR | diff --git a/foundation/factory-foundation/SKILL.md b/foundation/factory-foundation/SKILL.md index 8957430..f65c009 100644 --- a/foundation/factory-foundation/SKILL.md +++ b/foundation/factory-foundation/SKILL.md @@ -3,8 +3,8 @@ name: factory-foundation description: Prepare or assess a software project's engineering foundation and its execution host, including reproducible setup, real checks, documentation, CI/delivery, native harness access and GitHub work tracking. license: MIT metadata: - version: "0.1.19" - updated: "2026-10-21" + version: "0.1.20" + updated: "2026-10-22" --- # Factory Foundation @@ -73,6 +73,8 @@ Prepare only the chosen capabilities: - Stage and review selected skills before native project/profile adoption. Keep one canonical copy, verify discovery in each chosen harness and preserve local adaptations on updates. Never install personal context as a dependency. + For the owner's own AI agent, adopt `factory-entry` as in + [the setup guide](references/setup.md#the-owners-own-ai-agent). - Prepare meaningful project checks, work tracking and delivery protections. Preserve existing field/label conventions and read settings back from GitHub. A local workflow file does not prove service-side protection is enabled. diff --git a/foundation/factory-foundation/references/adoption.md b/foundation/factory-foundation/references/adoption.md index 23a490c..1e90238 100644 --- a/foundation/factory-foundation/references/adoption.md +++ b/foundation/factory-foundation/references/adoption.md @@ -1,6 +1,6 @@ # Adoption record -Revision: 0.1.19 · Updated: 2026-10-21 +Revision: 0.1.20 · Updated: 2026-10-22 Foundation identifies the appropriate existing project/adoption record and establishes the relevant choices. AgentOps maintains them and scoped exceptions @@ -85,6 +85,10 @@ Reuse the following existing choice families only where they matter: prevail. Refresh it when models, requirements or retained outcomes change. - Selected skills, Factory tag/revision, stage hashes, installed paths, preserved local changes and observed native discovery. +- The owner's own AI agent: which agent and harness hold `factory-entry`, and its + requested, installed, discovered and used states recorded separately. Also the + current lead for each project it routes to, so a resumed caller can find the + right recipient again. - Ops thread identity, project/workspace binding, role/source paths, pin and auto-settle choice; explicit source loading distinguished from native discovery. - Each client's saved service connection and Local environment choice; observed diff --git a/foundation/factory-foundation/references/setup.md b/foundation/factory-foundation/references/setup.md index adc0cd7..d4da455 100644 --- a/foundation/factory-foundation/references/setup.md +++ b/foundation/factory-foundation/references/setup.md @@ -1,6 +1,6 @@ # Set up the host and operator client -Revision: 0.1.19 · Updated: 2026-10-21 +Revision: 0.1.20 · Updated: 2026-10-22 This reference setup uses T3 Code on a Linux execution host, with a Mac and optionally a phone as clients. The host can be a laptop, workstation or VPS. @@ -426,6 +426,11 @@ Select scope deliberately: | Agent Ops | The selected project, activated in its Ops thread; use a separate project/profile/identity when its access needs differ | | ADLC | The application project, or the native user/config scope described below | +To stage every canonical skill as one flat set with one manifest, run +`python3 scripts/stage.py all --output "$factory_stage_root/all"`. It refuses a +missing group or a duplicate skill name, and keeps the same tracked-only, symlink +and fresh-destination controls. Copy from it only the skills each destination needs. + For Codex, use the native project location `.agents/skills/`. Its documented user location is `$HOME/.agents/skills`, shared across that OS user's projects; changing `CODEX_HOME` alone does not isolate that skill location. A project skill is discoverable @@ -466,9 +471,10 @@ requires deliberate reconciliation; failure of the command is not successful ado Before removing the temporary stage, retain its `manifest.json` in the persistent private adoption record, alongside the version, date and installed destination. The manifest is at the bundle root, not inside the copied skill directory. -Use `.claude/skills` instead for a project-scoped Claude installation. Load a new -native session and verify the correct skills appear and can read their references. -Ask it to assess a harmless example; copied files alone are not discovery proof. +Use `.claude/skills` instead for a project-scoped Claude installation. In the +intended harness and session, verify the correct skills appear and can read their +references. Ask it to assess a harmless example; copied files alone are not +discovery proof. Do not replace the application's AGENTS/CLAUDE instructions. Add a short route only where the native harness requires one, preserving the canonical source. @@ -477,6 +483,41 @@ canonical skill files directly without copying them back into their own source tree. Record this as explicit source loading, not native skill discovery. Configure the persistent thread with [the operator workspace guide](workspace.md). +### The owner's own AI agent + +The owner's everyday AI agent reaches Factory through `factory-entry`. It routes +each request to the right existing project lead, and loads `factory-foundation` +when the owner asks to set up or explain a host. Install both from the all-set +stage into that agent's own native skill scope, such as its user-level skill +location. Project leads keep their own installations. Keep the owner's agent and +personal connectors out of the execution account, as in +[Choose the boundary](#1-choose-the-boundary). + +Record four separate states in the adoption record: + +- **Requested:** the owner asked for the entry in a named agent and harness. +- **Installed:** the destination and the retained stage manifest. +- **Discovered:** a readback from the actual caller surface showing the skill is + listed or can be invoked. +- **Used:** a real request routed through it to the intended lead. + +Do not assume a client picks up a newly installed skill in a running session, or +that it needs a restart; read it back in the intended harness and session. If +the skill cannot be discovered, the agent may read the canonical file explicitly +from a reviewed checkout; record that as explicit loading. An inaccessible path is +a gap to report, not a reason to widen access. + +Factory ships portable canonical skills and this staging route. Codex documents +[plugins](https://developers.openai.com/plugins/build/plugins) as a distribution +unit for several skills, alongside [repository and user skills](https://learn.chatgpt.com/docs/build-skills). +Claude has its own [skill locations and invocation](https://code.claude.com/docs/en/skills). +A Codex plugin is not proof of T3 or Claude discovery. A plugin remains an optional +later packaging for a harness that needs one; none is delivered or installed here. + +To update, stage the new reviewed tag, compare it with the installed copy and its +retained manifest, then adopt it deliberately. To roll back, restore the prior +reviewed copy named by its manifest, as in [Removal and recovery](#removal-and-recovery). + ## 6. Prepare the repo and prove one delivery Follow [repository preparation](github.md). Record the two decision points, diff --git a/foundation/factory-foundation/references/workspace.md b/foundation/factory-foundation/references/workspace.md index c3ec16b..0b04a53 100644 --- a/foundation/factory-foundation/references/workspace.md +++ b/foundation/factory-foundation/references/workspace.md @@ -1,11 +1,16 @@ # AgentOps in your project -Revision: 0.1.4 · Updated: 2026-10-09 +Revision: 0.1.20 · Updated: 2026-10-22 Keep one pinned **AgentOps** thread in the existing project. Preserve another name if the owner has chosen one. The thread holds the lead role and current decisions; the repository keeps the instructions and skill sources. +The owner's own AI agent sends this project's work here through `factory-entry`, +not to the Factory repository's lead. Record this thread as the project's lead in +the adoption record so the caller can find it again after a resume; see +[the owner's own AI agent](setup.md#the-owners-own-ai-agent). + ## Where work runs ```mermaid diff --git a/scripts/stage.py b/scripts/stage.py index 7265aa0..eea0594 100644 --- a/scripts/stage.py +++ b/scripts/stage.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 -"""Stage one reviewed skill bundle into a fresh directory; never install it.""" +"""Stage one reviewed skill bundle, or all of them as one flat set, into a fresh +directory; never install it.""" import argparse import hashlib @@ -9,6 +10,7 @@ ROOT = Path(__file__).resolve().parents[1] BUNDLES = ("foundation", "agent-ops", "adlc") +ALL = "all" def source_info(source): @@ -34,18 +36,24 @@ def source_info(source): def stage(bundle, destination, source=ROOT): - if bundle not in BUNDLES: - raise ValueError("Select foundation, agent-ops or adlc") + if bundle != ALL and bundle not in BUNDLES: + raise ValueError("Select foundation, agent-ops, adlc or all") destination = Path(destination) source = Path(source) tracked, revision, modified = source_info(source) if tracked is not None and not {"LICENSE", "VERSION"}.issubset(tracked): raise ValueError("LICENSE and VERSION must be tracked in the source checkout") - skills = sorted(p.parent for p in (source / bundle).glob("*/SKILL.md")) - if tracked is not None: - skills = [p for p in skills if (p / "SKILL.md").relative_to(source).as_posix() in tracked] - if not skills: - raise ValueError("The selected bundle has no skills") + skills = [] + for group in BUNDLES if bundle == ALL else (bundle,): + found = sorted(p.parent for p in (source / group).glob("*/SKILL.md")) + if tracked is not None: + found = [p for p in found if (p / "SKILL.md").relative_to(source).as_posix() in tracked] + if not found: + raise ValueError(f"The {group} bundle has no skills") + skills.extend(found) + # A flat set must not let one group's skill silently replace another's. + if len({skill.name for skill in skills}) != len(skills): + raise ValueError("Skill names must be unique across the staged set") files = {} license_bytes = (source / "LICENSE").read_bytes() version = (source / "VERSION").read_text().strip() @@ -81,7 +89,7 @@ def stage(bundle, destination, source=ROOT): def main(): parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("bundle", choices=BUNDLES) + parser.add_argument("bundle", choices=BUNDLES + (ALL,)) parser.add_argument("--output", required=True, type=Path, help="New directory under an existing parent") args = parser.parse_args() diff --git a/tests/test_stage.py b/tests/test_stage.py index fedd789..98d2eab 100644 --- a/tests/test_stage.py +++ b/tests/test_stage.py @@ -2,6 +2,7 @@ import importlib.util import json from pathlib import Path +import re import shutil import subprocess import tempfile @@ -21,6 +22,94 @@ def source_fixture(self, root): shutil.copy2(ROOT / "VERSION", source / "VERSION") return source + def full_source_fixture(self, root): + source = self.source_fixture(root) + for group in module.BUNDLES: + if not (source / group).exists(): + shutil.copytree(ROOT / group, source / group) + return source + + def test_all_set_has_every_canonical_skill_once_and_stays_portable(self): + with tempfile.TemporaryDirectory() as temporary: + target = Path(temporary) / "all" + manifest = module.stage(module.ALL, target) + self.assertEqual(manifest["bundle"], module.ALL) + self.assertEqual(json.loads((target / "manifest.json").read_text()), manifest) + sources = [p.parent for group in module.BUNDLES + for p in (ROOT / group).glob("*/SKILL.md")] + self.assertIn("factory-entry", {p.name for p in sources}) + staged = sorted(p.name for p in (target / "skills").iterdir()) + self.assertEqual(staged, sorted(p.name for p in sources)) + actual = {p.relative_to(target).as_posix() for p in target.rglob("*") + if p.is_file() and p.name != "manifest.json"} + self.assertEqual(actual, set(manifest["files"])) + for name, digest in manifest["files"].items(): + self.assertEqual(hashlib.sha256((target / name).read_bytes()).hexdigest(), digest) + for skill in sources: + folder = target / "skills" / skill.name + self.assertTrue((folder / "SKILL.md").is_file()) + # Each staged resource is the single canonical source, byte for byte. + for name in manifest["files"]: + parts = Path(name).parts + if parts[1] == skill.name and parts[-1] != "LICENSE": + self.assertEqual((target / name).read_bytes(), + (skill / Path(*parts[2:])).read_bytes()) + # Every local link in a staged skill must resolve inside that skill. + for document in folder.rglob("*.md"): + text = re.sub(r"```.*?```", "", document.read_text(encoding="utf-8"), flags=re.S) + for raw in re.findall(r"\]\(([^)#]+)", text): + if "://" in raw or raw.startswith("mailto:"): + continue + resolved = (document.parent / raw).resolve() + self.assertTrue(resolved.is_file(), f"{document}: {raw}") + self.assertTrue(resolved.is_relative_to(folder.resolve()), + f"{document}: {raw}") + + def test_all_set_refuses_a_missing_group(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + source = self.source_fixture(root) + with self.assertRaises(ValueError): + module.stage(module.ALL, root / "output", source) + self.assertFalse((root / "output").exists()) + + def test_all_set_refuses_duplicate_skill_names(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + source = self.full_source_fixture(root) + shutil.copytree(source / "agent-ops/factory-agent-ops", + source / "adlc/factory-agent-ops") + with self.assertRaises(ValueError): + module.stage(module.ALL, root / "output", source) + self.assertFalse((root / "output").exists()) + + def test_all_set_refuses_symlinked_resource_in_any_group(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + source = self.full_source_fixture(root) + secret = root / "private" + secret.write_text("never stage") + (source / "adlc/factory-review/leak").symlink_to(secret) + with self.assertRaises(ValueError): + module.stage(module.ALL, root / "output", source) + self.assertFalse((root / "output").exists()) + + @unittest.skipUnless(shutil.which("git"), "Git provenance needs Git") + def test_all_set_stages_only_tracked_files_with_provenance(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + source = self.full_source_fixture(root) + subprocess.run(["git", "init", "-q", str(source)], check=True) + subprocess.run(["git", "add", "."], cwd=source, check=True) + subprocess.run(["git", "-c", "user.name=Fixture", "-c", + "user.email=fixture@example.invalid", "-c", "commit.gpgsign=false", + "commit", "-qm", "Fixture"], cwd=source, check=True) + (source / "foundation/factory-foundation/untracked.txt").write_text("not reviewed") + manifest = module.stage(module.ALL, root / "output", source) + self.assertIsNotNone(manifest["sourceRevision"]) + self.assertTrue(manifest["sourceModified"]) + self.assertFalse(any(name.endswith("untracked.txt") for name in manifest["files"])) + @unittest.skipUnless(shutil.which("git"), "Git provenance needs Git") def test_archive_inside_unrelated_repo_has_no_git_provenance(self): with tempfile.TemporaryDirectory() as temporary: