dirigent-cli 0.24.0__tar.gz → 0.25.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. {dirigent_cli-0.24.0 → dirigent_cli-0.25.1}/PKG-INFO +9 -9
  2. {dirigent_cli-0.24.0 → dirigent_cli-0.25.1}/pyproject.toml +9 -9
  3. {dirigent_cli-0.24.0 → dirigent_cli-0.25.1}/pyproject.toml.orig +9 -9
  4. {dirigent_cli-0.24.0 → dirigent_cli-0.25.1}/src/dirigent_cli/commands.py +98 -15
  5. {dirigent_cli-0.24.0 → dirigent_cli-0.25.1}/src/dirigent_cli/init_form.py +2 -0
  6. {dirigent_cli-0.24.0 → dirigent_cli-0.25.1}/src/dirigent_cli/messages.py +4 -0
  7. {dirigent_cli-0.24.0 → dirigent_cli-0.25.1}/src/dirigent_cli/project.py +86 -1
  8. {dirigent_cli-0.24.0 → dirigent_cli-0.25.1}/src/dirigent_cli/summaries.py +1 -0
  9. {dirigent_cli-0.24.0 → dirigent_cli-0.25.1}/LICENSE +0 -0
  10. {dirigent_cli-0.24.0 → dirigent_cli-0.25.1}/README.md +0 -0
  11. {dirigent_cli-0.24.0 → dirigent_cli-0.25.1}/src/dirigent_cli/__init__.py +0 -0
  12. {dirigent_cli-0.24.0 → dirigent_cli-0.25.1}/src/dirigent_cli/aliases.py +0 -0
  13. {dirigent_cli-0.24.0 → dirigent_cli-0.25.1}/src/dirigent_cli/context.py +0 -0
  14. {dirigent_cli-0.24.0 → dirigent_cli-0.25.1}/src/dirigent_cli/explain.py +0 -0
  15. {dirigent_cli-0.24.0 → dirigent_cli-0.25.1}/src/dirigent_cli/formatters.py +0 -0
  16. {dirigent_cli-0.24.0 → dirigent_cli-0.25.1}/src/dirigent_cli/graph.py +0 -0
  17. {dirigent_cli-0.24.0 → dirigent_cli-0.25.1}/src/dirigent_cli/health.py +0 -0
  18. {dirigent_cli-0.24.0 → dirigent_cli-0.25.1}/src/dirigent_cli/local.py +0 -0
  19. {dirigent_cli-0.24.0 → dirigent_cli-0.25.1}/src/dirigent_cli/main.py +0 -0
  20. {dirigent_cli-0.24.0 → dirigent_cli-0.25.1}/src/dirigent_cli/output.py +0 -0
  21. {dirigent_cli-0.24.0 → dirigent_cli-0.25.1}/src/dirigent_cli/params.py +0 -0
  22. {dirigent_cli-0.24.0 → dirigent_cli-0.25.1}/src/dirigent_cli/profiles.py +0 -0
  23. {dirigent_cli-0.24.0 → dirigent_cli-0.25.1}/src/dirigent_cli/py.typed +0 -0
  24. {dirigent_cli-0.24.0 → dirigent_cli-0.25.1}/src/dirigent_cli/reaper.py +0 -0
  25. {dirigent_cli-0.24.0 → dirigent_cli-0.25.1}/src/dirigent_cli/scaffold.py +0 -0
  26. {dirigent_cli-0.24.0 → dirigent_cli-0.25.1}/src/dirigent_cli/schemas.py +0 -0
  27. {dirigent_cli-0.24.0 → dirigent_cli-0.25.1}/src/dirigent_cli/seeding.py +0 -0
  28. {dirigent_cli-0.24.0 → dirigent_cli-0.25.1}/src/dirigent_cli/source_commands.py +0 -0
  29. {dirigent_cli-0.24.0 → dirigent_cli-0.25.1}/src/dirigent_cli/sources.py +0 -0
  30. {dirigent_cli-0.24.0 → dirigent_cli-0.25.1}/src/dirigent_cli/starters.py +0 -0
  31. {dirigent_cli-0.24.0 → dirigent_cli-0.25.1}/src/dirigent_cli/stream.py +0 -0
  32. {dirigent_cli-0.24.0 → dirigent_cli-0.25.1}/src/dirigent_cli/templates/pack/README.md.tmpl +0 -0
  33. {dirigent_cli-0.24.0 → dirigent_cli-0.25.1}/src/dirigent_cli/templates/pack/__init__.py.tmpl +0 -0
  34. {dirigent_cli-0.24.0 → dirigent_cli-0.25.1}/src/dirigent_cli/templates/pack/operator.py.tmpl +0 -0
  35. {dirigent_cli-0.24.0 → dirigent_cli-0.25.1}/src/dirigent_cli/templates/pack/pyproject.toml.tmpl +0 -0
  36. {dirigent_cli-0.24.0 → dirigent_cli-0.25.1}/src/dirigent_cli/templates/pack/test_plugin.py.tmpl +0 -0
  37. {dirigent_cli-0.24.0 → dirigent_cli-0.25.1}/src/dirigent_cli/timing.py +0 -0
  38. {dirigent_cli-0.24.0 → dirigent_cli-0.25.1}/src/dirigent_cli/triggers.py +0 -0
@@ -1,19 +1,19 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dirigent-cli
3
- Version: 0.24.0
3
+ Version: 0.25.1
4
4
  Summary: The dirigent command line interface (dirigent / dg).
5
5
  License-Expression: LicenseRef-Proprietary
6
6
  License-File: LICENSE
7
7
  Classifier: Programming Language :: Python :: 3
8
8
  Classifier: Programming Language :: Python :: 3.13
9
- Requires-Dist: dirigent-block-execute==0.24.0
10
- Requires-Dist: dirigent-blocks==0.24.0
11
- Requires-Dist: dirigent-client==0.24.0
12
- Requires-Dist: dirigent-common==0.24.0
13
- Requires-Dist: dirigent-core==0.24.0
14
- Requires-Dist: dirigent-examples==0.24.0
15
- Requires-Dist: dirigent-plugin==0.24.0
16
- Requires-Dist: dirigent-server==0.24.0
9
+ Requires-Dist: dirigent-block-execute==0.25.1
10
+ Requires-Dist: dirigent-blocks==0.25.1
11
+ Requires-Dist: dirigent-client==0.25.1
12
+ Requires-Dist: dirigent-common==0.25.1
13
+ Requires-Dist: dirigent-core==0.25.1
14
+ Requires-Dist: dirigent-examples==0.25.1
15
+ Requires-Dist: dirigent-plugin==0.25.1
16
+ Requires-Dist: dirigent-server==0.25.1
17
17
  Requires-Dist: httpx2>=2.12.0
18
18
  Requires-Dist: python-dotenv>=1.1.0
19
19
  Requires-Dist: pyyaml>=6.0.3
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "dirigent-cli"
3
- version = "0.24.0"
3
+ version = "0.25.1"
4
4
  description = "The dirigent command line interface (dirigent / dg)."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
@@ -11,14 +11,14 @@ classifiers = [
11
11
  "Programming Language :: Python :: 3.13",
12
12
  ]
13
13
  dependencies = [
14
- "dirigent-block-execute==0.24.0",
15
- "dirigent-blocks==0.24.0",
16
- "dirigent-client==0.24.0",
17
- "dirigent-common==0.24.0",
18
- "dirigent-core==0.24.0",
19
- "dirigent-examples==0.24.0",
20
- "dirigent-plugin==0.24.0",
21
- "dirigent-server==0.24.0",
14
+ "dirigent-block-execute==0.25.1",
15
+ "dirigent-blocks==0.25.1",
16
+ "dirigent-client==0.25.1",
17
+ "dirigent-common==0.25.1",
18
+ "dirigent-core==0.25.1",
19
+ "dirigent-examples==0.25.1",
20
+ "dirigent-plugin==0.25.1",
21
+ "dirigent-server==0.25.1",
22
22
  "httpx2>=2.12.0",
23
23
  "python-dotenv>=1.1.0",
24
24
  "pyyaml>=6.0.3",
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "dirigent-cli"
3
- version = "0.24.0"
3
+ version = "0.25.1"
4
4
  description = "The dirigent command line interface (dirigent / dg)."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
@@ -11,14 +11,14 @@ classifiers = [
11
11
  "Programming Language :: Python :: 3.13",
12
12
  ]
13
13
  dependencies = [
14
- "dirigent-block-execute==0.24.0",
15
- "dirigent-blocks==0.24.0",
16
- "dirigent-client==0.24.0",
17
- "dirigent-common==0.24.0",
18
- "dirigent-core==0.24.0",
19
- "dirigent-examples==0.24.0",
20
- "dirigent-plugin==0.24.0",
21
- "dirigent-server==0.24.0",
14
+ "dirigent-block-execute==0.25.1",
15
+ "dirigent-blocks==0.25.1",
16
+ "dirigent-client==0.25.1",
17
+ "dirigent-common==0.25.1",
18
+ "dirigent-core==0.25.1",
19
+ "dirigent-examples==0.25.1",
20
+ "dirigent-plugin==0.25.1",
21
+ "dirigent-server==0.25.1",
22
22
  "httpx2>=2.12.0",
23
23
  "python-dotenv>=1.1.0",
24
24
  "pyyaml>=6.0.3",
@@ -1,6 +1,7 @@
1
1
  """The commands that talk to a server: definitions, execution, catalog, connections, admin."""
2
2
 
3
3
  import asyncio
4
+ import json
4
5
  import os
5
6
  import sys
6
7
  from collections.abc import Callable, Iterator, Mapping, Sequence
@@ -58,6 +59,7 @@ from dirigent_cli.messages import (
58
59
  NO_FAILURE_TO_RETRY,
59
60
  NO_FIRST_PASSWORD,
60
61
  NO_REPORT_DOCUMENT,
62
+ NO_SUCH_BLOCK,
61
63
  NO_SUCH_EXAMPLE,
62
64
  NOT_A_BLOCK_KIND,
63
65
  NOT_A_PRIORITY,
@@ -122,6 +124,7 @@ from dirigent_cli.project import (
122
124
  DEFAULT_PIPELINES_DIR,
123
125
  InitChoices,
124
126
  ProjectError,
127
+ agent_notes,
125
128
  check_choices,
126
129
  find_project,
127
130
  scaffold,
@@ -500,10 +503,14 @@ def validate_command(
500
503
  bool, typer.Option("--explain", help="Also say what the work will cost: how wide, how many tries, how long.")
501
504
  ] = False,
502
505
  ) -> None:
503
- """Check a document offline, or against a server's catalog too with --server.
506
+ """Check a document against the blocks installed beside dg, or against a server's with --server.
504
507
 
505
- Offline, a triggers document is checked at the schema level: its clocks and its codes.
506
- `--server` adds the pipeline it names and the parameters its schedules pin.
508
+ Offline, every step's block and config is checked against the blocks installed beside dg,
509
+ and a connection or pipeline a document names is not, since only an instance knows what it
510
+ holds; a triggers document is checked at the schema level, its clocks and its codes.
511
+ `--server` checks against the instance's own catalog instead, and adds the connections and
512
+ pipelines it holds, the pipeline a triggers document names, and the parameters its
513
+ schedules pin.
507
514
 
508
515
  `--explain` adds the shape of the work ahead for every pipeline document that is valid:
509
516
  the fan-out widths, the retry budgets and the waits. With `--server` too, a sensor that
@@ -525,11 +532,18 @@ def validate_command(
525
532
  problems=[issue.model_dump(mode="json") for issue in error.problems],
526
533
  )
527
534
  failures += 1
528
- catalog: Catalog | None = None
529
- connections: list[str] = []
530
- pipelines: list[str] = []
535
+ from dirigent_common import format_checker_with
536
+ from dirigent_core.plugins import load_plugin_host
537
+
538
+ host = load_plugin_host()
539
+ catalog = host.catalog()
540
+ blocks: Mapping[str, Any] | None = host.blocks
541
+ checker = format_checker_with(host.formats)
542
+ connections: list[str] | None = None
543
+ pipelines: list[str] | None = None
531
544
  targets: dict[str, TriggerTarget] = {}
532
545
  if server:
546
+ blocks = None
533
547
  with client_for(state) as dg:
534
548
  catalog = dg.call(dg.blocks.catalog())
535
549
  connections = [
@@ -543,10 +557,11 @@ def validate_command(
543
557
  for definition, label in parsed:
544
558
  issues = validate_against_catalog(
545
559
  definition,
546
- catalog or Catalog(),
560
+ catalog,
547
561
  connections=connections,
548
562
  pipelines=pipelines,
549
- check_blocks=catalog is not None,
563
+ blocks=blocks,
564
+ format_checker=checker,
550
565
  target=targets.get(definition.pipeline) if isinstance(definition, TriggersDefinition) else None,
551
566
  )
552
567
  if issues:
@@ -560,7 +575,7 @@ def validate_command(
560
575
  )
561
576
  failures += 1
562
577
  continue
563
- checked = "document and catalog" if catalog is not None else "document, offline"
578
+ checked = "document and catalog" if server else "document and installed blocks"
564
579
  emit_fact(
565
580
  "validation",
566
581
  message="valid",
@@ -630,6 +645,10 @@ def init_command(
630
645
  ] = None,
631
646
  admin: Annotated[str, typer.Option(help="The first admin account's username.")] = "admin",
632
647
  password: Annotated[str | None, typer.Option(help="Its password; asked for when omitted.")] = None,
648
+ agents: Annotated[
649
+ bool,
650
+ typer.Option("--agents", help="Write only AGENTS.md and CLAUDE.md, into a project that already exists."),
651
+ ] = False,
633
652
  ) -> None:
634
653
  """Initialise a uv project and the instance it addresses, ready for `dg dev`.
635
654
 
@@ -640,7 +659,8 @@ def init_command(
640
659
  and mints it a token and a secret key, both kept in `.env`. `--template compose` writes the
641
660
  documents and a container stack instead, and initialises nothing locally: the instance is
642
661
  the containers. `--template documents` writes the documents alone, against an instance
643
- somebody else runs.
662
+ somebody else runs. `--agents` writes a coding agent's notes alone, into a project made
663
+ before `dg init` wrote them, and keeps any the project already has.
644
664
  """
645
665
  import asyncio
646
666
 
@@ -649,6 +669,20 @@ def init_command(
649
669
  root = directory.resolve()
650
670
  version = cli_version()
651
671
  unasked = template is None and service is None and pack is None and pipeline is None and not workflow
672
+ if agents:
673
+ if not unasked or password is not None or admin != "admin":
674
+ _init_fail(INIT_REFUSED, detail="--agents writes the notes alone and takes no other option")
675
+ if not directory.is_dir():
676
+ _init_fail(INIT_REFUSED, detail=f"{directory} is not a directory; --agents writes into a project")
677
+ notes = agent_notes(directory)
678
+ emit_fact(
679
+ "project.scaffolded",
680
+ message="scaffolded",
681
+ directory=str(directory),
682
+ files=[_within(path, directory) for path in notes.files],
683
+ **({"skipped": [_within(path, directory) for path in notes.skipped]} if notes.skipped else {}),
684
+ )
685
+ return
652
686
  asked = unasked and sys.stdin.isatty() and sys.stdout.isatty()
653
687
  # Refusing happens before anything is written or asked for, so a run that cannot finish
654
688
  # has not half-created a project, and nobody fills a form for a command that was going
@@ -963,6 +997,29 @@ def pipeline_validate(
963
997
  raise typer.Exit(code=1)
964
998
 
965
999
 
1000
+ @pipeline_app.command("schema")
1001
+ def pipeline_schema(
1002
+ file: Annotated[
1003
+ Path | None, typer.Option("-f", "--file", help="Write the schema to this file instead of into the record.")
1004
+ ] = None,
1005
+ ) -> None:
1006
+ """Compose the JSON Schema a document is written against, from the blocks installed beside dg.
1007
+
1008
+ It is the format and every installed block's config in one Draft 2020-12 schema, which an
1009
+ editor or a language model checks a document against before anything validates it. Written
1010
+ to a file, it is what a ``# yaml-language-server: $schema=`` comment names.
1011
+ """
1012
+ from dirigent_core.documentschema import document_schema
1013
+
1014
+ catalog = installed_catalog()
1015
+ schema = document_schema(catalog)
1016
+ if file is not None:
1017
+ file.parent.mkdir(parents=True, exist_ok=True)
1018
+ file.write_text(json.dumps(schema, indent=2) + "\n")
1019
+ return emit_fact("pipeline.schema", message="written", path=str(file), blocks=len(catalog.blocks))
1020
+ emit_fact("pipeline.schema", message="composed", blocks=len(catalog.blocks), schema=schema)
1021
+
1022
+
966
1023
  @pipeline_app.command("activate")
967
1024
  def pipeline_activate(ctx: typer.Context, code: Annotated[str, typer.Argument()]) -> None:
968
1025
  """Make a pipeline runnable again."""
@@ -2308,16 +2365,35 @@ def blocks_new(
2308
2365
  console.print(" [bold]uv sync && uv run pytest[/]")
2309
2366
 
2310
2367
 
2368
+ #: What ``--local`` means on the block commands.
2369
+ LOCAL_BLOCKS = "Read the blocks installed beside dg, instead of the instance's own."
2370
+
2371
+
2372
+ def installed_catalog() -> Catalog:
2373
+ """Read the catalog every plugin installed beside this `dg` contributes, without addressing any server."""
2374
+ from dirigent_core.plugins import load_plugin_host
2375
+
2376
+ return load_plugin_host().catalog()
2377
+
2378
+
2311
2379
  @blocks_app.command("list")
2312
2380
  def blocks_list(
2313
2381
  ctx: typer.Context,
2314
2382
  kind: Annotated[str | None, typer.Option(help="Only operators, or only sensors.")] = None,
2383
+ local: Annotated[bool, typer.Option("--local", help=LOCAL_BLOCKS)] = False,
2315
2384
  ) -> None:
2316
- """List the blocks this instance can run."""
2385
+ """List the blocks this instance can run, or with --local the ones installed beside dg."""
2317
2386
  if kind is not None and kind not in set(BlockKind):
2318
2387
  fail(NOT_A_BLOCK_KIND, kind=repr(kind), allowed=", ".join(sorted(BlockKind)))
2319
- with client_for(state_of(ctx)) as dg:
2320
- catalog = dg.call(dg.blocks.catalog(kind=BlockKind(kind) if kind else None))
2388
+ wanted = BlockKind(kind) if kind else None
2389
+ if local:
2390
+ catalog = installed_catalog()
2391
+ catalog = catalog.model_copy(
2392
+ update={"blocks": [block for block in catalog.blocks if wanted is None or block.kind is wanted]}
2393
+ )
2394
+ else:
2395
+ with client_for(state_of(ctx)) as dg:
2396
+ catalog = dg.call(dg.blocks.catalog(kind=wanted))
2321
2397
  if state_of(ctx).json_output:
2322
2398
  emit_records("block", catalog.blocks)
2323
2399
  emit_records("storage_scheme", catalog.storage_schemes)
@@ -2338,10 +2414,17 @@ def blocks_list(
2338
2414
  def blocks_show(
2339
2415
  ctx: typer.Context,
2340
2416
  block_id: Annotated[str, typer.Argument(help="The block to describe.")],
2417
+ local: Annotated[bool, typer.Option("--local", help=LOCAL_BLOCKS)] = False,
2341
2418
  ) -> None:
2342
2419
  """Show one block's config and output schemas."""
2343
- with client_for(state_of(ctx)) as dg:
2344
- entry = dg.call(dg.blocks.get(block_id))
2420
+ if local:
2421
+ found = [block for block in installed_catalog().blocks if block.id == block_id]
2422
+ if not found:
2423
+ fail(NO_SUCH_BLOCK, block=repr(block_id))
2424
+ entry = found[0]
2425
+ else:
2426
+ with client_for(state_of(ctx)) as dg:
2427
+ entry = dg.call(dg.blocks.get(block_id))
2345
2428
  if state_of(ctx).json_output:
2346
2429
  return emit_one("block", entry)
2347
2430
  fields(
@@ -238,6 +238,8 @@ class InitForm(App[InitChoices | None]):
238
238
  ".dirigent/profiles.yaml",
239
239
  "pyproject.toml",
240
240
  "README.md",
241
+ "AGENTS.md",
242
+ "CLAUDE.md",
241
243
  ]
242
244
  if choices.stack:
243
245
  files += ["compose.yaml", "Dockerfile", ".env"]
@@ -335,3 +335,7 @@ SOURCE_NOT_CALLABLE = CLI.define(
335
335
  )
336
336
 
337
337
  SOURCE_UNREACHABLE = CLI.define("source_unreachable", "{url} could not be called: {detail}")
338
+
339
+ NO_SUCH_BLOCK = CLI.define(
340
+ "no_such_block", "no block {block} is installed beside dg; dg blocks list --local names them"
341
+ )
@@ -842,7 +842,8 @@ README_TEMPLATE = """\
842
842
  # __NAME__
843
843
 
844
844
  A dirigent project. `pipelines/` holds the pipeline documents and `dirigent.yaml` the
845
- project's settings.
845
+ project's settings. `AGENTS.md` says how to find, check and run a document, for a person or a
846
+ coding agent working here.
846
847
 
847
848
  `pyproject.toml` pins the dirigent runtime, so the `dg` this project runs on is that pinned
848
849
  one and `uv run` is how it is reached.
@@ -854,6 +855,75 @@ __RUN__
854
855
  ```
855
856
  """
856
857
 
858
+ AGENTS_TEMPLATE = """\
859
+ # Working in __NAME__
860
+
861
+ This is a dirigent project: pipelines are YAML documents under `pipelines/`, checked and run by
862
+ the `dg` command line this project pins. These notes are for whoever edits it, a person or a
863
+ coding agent.
864
+
865
+ ## The loop
866
+
867
+ Every command goes through `uv run dg`, so it runs the dirigent version `pyproject.toml` pins.
868
+
869
+ 1. **Find what exists before writing anything.**
870
+ - `uv run dg examples list --local` lists the documents every installed plugin ships, and
871
+ `uv run dg examples show CODE --local` prints one verbatim. Copy from the closest one.
872
+ - `uv run dg pipeline new STARTER` copies a starter into `pipelines/` as a pipeline of its own;
873
+ `uv run dg examples list --local --starter` names the starters.
874
+ - `uv run dg blocks list --local` lists every block, and `uv run dg blocks show BLOCK --local`
875
+ gives its config and output schemas.
876
+ - `uv run dg pipeline schema -f .dirigent/document.schema.json` writes the JSON Schema a
877
+ document is checked against, every installed block's config included. A document whose first
878
+ line is `# yaml-language-server: $schema=../.dirigent/document.schema.json` is checked by any
879
+ editor that reads that comment.
880
+ 2. **Check it offline:** `uv run dg validate pipelines/NAME.yaml`. `--explain` adds how wide the
881
+ work is, how many tries it may take and how long it may run.
882
+ 3. **Run it in a throwaway instance:** `uv run dg run --local pipelines/NAME.yaml`, with `-v` to
883
+ see every step's output. A pipeline that starts at a source takes `--input '{...}'`, or runs
884
+ with its source's `sample`.
885
+ 4. **Apply it to an instance** once it runs: `uv run dg apply`, then `uv run dg run CODE --watch`.
886
+ `uv run dg dev` starts a local instance with its web UI; it keeps running, so a person starts it
887
+ in a terminal of its own.
888
+
889
+ ## Reading what dg says
890
+
891
+ Outside a terminal, which is every agent's shell, every `dg` command writes NDJSON: one JSON
892
+ record per line, each with a `kind`. Select with `jq`, for example
893
+ `uv run dg run --local pipelines/NAME.yaml | jq -c 'select(.kind == "run")'`.
894
+
895
+ - A document that does not validate is a `validation` record with `message` `invalid` and
896
+ `problems[]`, each with a stable `code`, a `message` and the `location` in the document it is
897
+ about. Fix what `location` names, then validate again.
898
+ - Anything else refused is a record of kind `error`, with a stable `code`, the `params` it was
899
+ built from, and `problems[]` where there is more than one thing to say.
900
+ - A run's stream is `run`, `step`, `log` and `output` records, and the closing `run` record
901
+ carries every step's outcome, its output and the failures. `exit_code` is the command's own.
902
+ - `uv run dg runs show RUN_ID`, `uv run dg runs logs RUN_ID` and `uv run dg runs profile RUN_ID`
903
+ read a run an instance holds.
904
+
905
+ ## Rules that are easy to get wrong
906
+
907
+ - A document starts `format: dirigent/v1`, `kind: pipeline`, and a `code` in lowercase with
908
+ dashes. A step is exactly one of `block`, `source` or `reply`.
909
+ - Values move between steps by reference, never by copying: `${params.NAME}`,
910
+ `${steps.STEP.output.FIELD}`, and `${item}` inside a `for_each`. A step that reads another's
911
+ output lists it under `depends_on`.
912
+ - Secrets never go in a document. A connection holds them, created once with
913
+ `uv run dg connection create KIND CODE`, and a step names it by its code.
914
+ - `.dirigent/state/` and `.env` belong to the local instance; they are not edited by hand.
915
+
916
+ ## Reading further
917
+
918
+ The documentation is at https://winterop-com.github.io/dirigent/, and
919
+ https://winterop-com.github.io/dirigent/llms.txt lists every page with what it covers. The
920
+ installed examples are the most reliable reference, because each one is run by dirigent's own
921
+ tests.
922
+ """
923
+
924
+ #: Claude Code reads CLAUDE.md, and this makes it read the same notes every other agent does.
925
+ CLAUDE_TEMPLATE = "@AGENTS.md\n"
926
+
857
927
  #: The commands a scaffolded project's README opens with, per template.
858
928
  README_RUN: Final = {
859
929
  "local": (
@@ -909,6 +979,7 @@ def scaffold(directory: Path, choices: InitChoices, *, version: str = "0.0.0") -
909
979
  _record(directory / "pyproject.toml", pyproject_document(name, choices, version), written, skipped)
910
980
  readme = README_TEMPLATE.replace("__NAME__", name).replace("__RUN__", "\n".join(README_RUN[choices.template]))
911
981
  _record(directory / "README.md", readme, written, skipped)
982
+ _record_agent_notes(directory, written, skipped)
912
983
  _merge_ignore(directory / ".gitignore", ROOT_IGNORE_TEMPLATE, written, skipped)
913
984
  if choices.workflow:
914
985
  written.append(_write(directory / ".github" / "workflows" / "dirigent.yaml", WORKFLOW_TEMPLATE))
@@ -919,6 +990,20 @@ def scaffold(directory: Path, choices: InitChoices, *, version: str = "0.0.0") -
919
990
  return Scaffolded(files=written, skipped=skipped)
920
991
 
921
992
 
993
+ def agent_notes(directory: Path) -> Scaffolded:
994
+ """Write a coding agent's notes into a project, leaving any it already has alone."""
995
+ written: list[Path] = []
996
+ skipped: list[Path] = []
997
+ _record_agent_notes(directory, written, skipped)
998
+ return Scaffolded(files=written, skipped=skipped)
999
+
1000
+
1001
+ def _record_agent_notes(directory: Path, written: list[Path], skipped: list[Path]) -> None:
1002
+ """Write AGENTS.md, and the CLAUDE.md that points at it, unless the project has its own."""
1003
+ _record(directory / "AGENTS.md", AGENTS_TEMPLATE.replace("__NAME__", project_name(directory)), written, skipped)
1004
+ _record(directory / "CLAUDE.md", CLAUDE_TEMPLATE, written, skipped)
1005
+
1006
+
922
1007
  def write_instance_env(directory: Path, token: str) -> Path:
923
1008
  """Write the ``.env`` holding a new instance's token and key, readable by its owner alone."""
924
1009
  document = INSTANCE_ENV_TEMPLATE.replace("__TOKEN__", token).replace("__SECRET_KEY__", _an_instance_key())
@@ -50,6 +50,7 @@ BULKY: Final = frozenset(
50
50
  "issues",
51
51
  "files",
52
52
  "token",
53
+ "schema",
53
54
  "preflight",
54
55
  "requires",
55
56
  "used_by",
File without changes
File without changes