prodc 0.3.64__tar.gz → 0.3.65__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 (53) hide show
  1. {prodc-0.3.64 → prodc-0.3.65}/PKG-INFO +1 -1
  2. {prodc-0.3.64 → prodc-0.3.65}/pyproject.toml +1 -1
  3. prodc-0.3.65/src/prodc/cli/__init__.py +16 -0
  4. prodc-0.3.65/src/prodc/cli/app.py +439 -0
  5. prodc-0.3.65/src/prodc/cli/commands.py +926 -0
  6. prodc-0.3.65/src/prodc/cli/common.py +102 -0
  7. prodc-0.3.65/src/prodc/cli/render.py +389 -0
  8. prodc-0.3.65/src/prodc/cli/serialize.py +211 -0
  9. prodc-0.3.64/src/prodc/cli.py +0 -1968
  10. {prodc-0.3.64 → prodc-0.3.65}/.gitignore +0 -0
  11. {prodc-0.3.64 → prodc-0.3.65}/CHANGELOG.md +0 -0
  12. {prodc-0.3.64 → prodc-0.3.65}/LICENSE +0 -0
  13. {prodc-0.3.64 → prodc-0.3.65}/README.md +0 -0
  14. {prodc-0.3.64 → prodc-0.3.65}/examples/quickstart/features/orders.feature +0 -0
  15. {prodc-0.3.64 → prodc-0.3.65}/examples/quickstart/needs/jordan.yaml +0 -0
  16. {prodc-0.3.64 → prodc-0.3.65}/examples/quickstart/prodc.toml +0 -0
  17. {prodc-0.3.64 → prodc-0.3.65}/src/prodc/__init__.py +0 -0
  18. {prodc-0.3.64 → prodc-0.3.65}/src/prodc/adapters.py +0 -0
  19. {prodc-0.3.64 → prodc-0.3.65}/src/prodc/agent.md +0 -0
  20. {prodc-0.3.64 → prodc-0.3.65}/src/prodc/board.py +0 -0
  21. {prodc-0.3.64 → prodc-0.3.65}/src/prodc/callbacks.py +0 -0
  22. {prodc-0.3.64 → prodc-0.3.65}/src/prodc/config.py +0 -0
  23. {prodc-0.3.64 → prodc-0.3.65}/src/prodc/dedup.py +0 -0
  24. {prodc-0.3.64 → prodc-0.3.65}/src/prodc/loader.py +0 -0
  25. {prodc-0.3.64 → prodc-0.3.65}/src/prodc/model.py +0 -0
  26. {prodc-0.3.64 → prodc-0.3.65}/src/prodc/py.typed +0 -0
  27. {prodc-0.3.64 → prodc-0.3.65}/src/prodc/report.py +0 -0
  28. {prodc-0.3.64 → prodc-0.3.65}/src/prodc/report_html.py +0 -0
  29. {prodc-0.3.64 → prodc-0.3.65}/src/prodc/status.py +0 -0
  30. {prodc-0.3.64 → prodc-0.3.65}/src/prodc/writer.py +0 -0
  31. {prodc-0.3.64 → prodc-0.3.65}/tests/test_adapters.py +0 -0
  32. {prodc-0.3.64 → prodc-0.3.65}/tests/test_callbacks.py +0 -0
  33. {prodc-0.3.64 → prodc-0.3.65}/tests/test_cli.py +0 -0
  34. {prodc-0.3.64 → prodc-0.3.65}/tests/test_command_hook.py +0 -0
  35. {prodc-0.3.64 → prodc-0.3.65}/tests/test_dedup.py +0 -0
  36. {prodc-0.3.64 → prodc-0.3.65}/tests/test_description.py +0 -0
  37. {prodc-0.3.64 → prodc-0.3.65}/tests/test_drafts.py +0 -0
  38. {prodc-0.3.64 → prodc-0.3.65}/tests/test_external_adapter.py +0 -0
  39. {prodc-0.3.64 → prodc-0.3.65}/tests/test_gitlab_source.py +0 -0
  40. {prodc-0.3.64 → prodc-0.3.65}/tests/test_importance.py +0 -0
  41. {prodc-0.3.64 → prodc-0.3.65}/tests/test_loader.py +0 -0
  42. {prodc-0.3.64 → prodc-0.3.65}/tests/test_models.py +0 -0
  43. {prodc-0.3.64 → prodc-0.3.65}/tests/test_report.py +0 -0
  44. {prodc-0.3.64 → prodc-0.3.65}/tests/test_report_html.py +0 -0
  45. {prodc-0.3.64 → prodc-0.3.65}/tests/test_roles.py +0 -0
  46. {prodc-0.3.64 → prodc-0.3.65}/tests/test_sources.py +0 -0
  47. {prodc-0.3.64 → prodc-0.3.65}/tests/test_staleness.py +0 -0
  48. {prodc-0.3.64 → prodc-0.3.65}/tests/test_status.py +0 -0
  49. {prodc-0.3.64 → prodc-0.3.65}/tests/test_suggest.py +0 -0
  50. {prodc-0.3.64 → prodc-0.3.65}/tests/test_variants.py +0 -0
  51. {prodc-0.3.64 → prodc-0.3.65}/tests/test_who_variants.py +0 -0
  52. {prodc-0.3.64 → prodc-0.3.65}/tests/test_writer.py +0 -0
  53. {prodc-0.3.64 → prodc-0.3.65}/tests/test_writer_comments.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: prodc
3
- Version: 0.3.64
3
+ Version: 0.3.65
4
4
  Summary: Product management as code: roles, flows, and features traced to evidence.
5
5
  Project-URL: Homepage, https://gitlab.com/jorgeecardona/prodc
6
6
  Project-URL: Repository, https://gitlab.com/jorgeecardona/prodc
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "prodc"
3
- version = "0.3.64"
3
+ version = "0.3.65"
4
4
  description = "Product management as code: roles, flows, and features traced to evidence."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -0,0 +1,16 @@
1
+ """``prodc`` command line: the board (``status``) and its sibling commands.
2
+
3
+ Prints the gap between needs and what the test suite proves, sorted by what needs
4
+ attention first. Exit 1 on a BROKEN proof; ``--strict`` also on an opinion. A HOLE
5
+ never fails — it is the honest gap you want people to keep writing.
6
+
7
+ This package splits the former single ``cli`` module into ``app`` (argparse + dispatch),
8
+ ``commands`` (the handlers), ``render`` (text renderers), ``serialize`` (JSON), and
9
+ ``common`` (shared helpers). The public surface is unchanged: ``from prodc.cli import main``.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from .app import main
15
+
16
+ __all__ = ["main"]
@@ -0,0 +1,439 @@
1
+ """The argument parser and dispatch for ``prodc`` — one subparser per command.
2
+
3
+ Builds the argparse tree, wires each subcommand to its handler in :mod:`prodc.cli.commands`,
4
+ and runs it. ``main`` is the console entry point (``prodc.cli:main``), re-exported from the
5
+ package ``__init__``.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import os
12
+ import sys
13
+
14
+ from .. import __version__
15
+ from .commands import (
16
+ cmd_accept,
17
+ cmd_add,
18
+ cmd_agent,
19
+ cmd_argument,
20
+ cmd_attest,
21
+ cmd_brief,
22
+ cmd_capture,
23
+ cmd_diff,
24
+ cmd_dupes,
25
+ cmd_lint,
26
+ cmd_plan,
27
+ cmd_portfolio,
28
+ cmd_recurrence,
29
+ cmd_report,
30
+ cmd_set,
31
+ cmd_show,
32
+ cmd_status,
33
+ cmd_suggest,
34
+ cmd_sync,
35
+ )
36
+
37
+
38
+ def main(argv: list[str] | None = None) -> int:
39
+ parser = argparse.ArgumentParser(prog="prodc", description="Product management as code.")
40
+ parser.add_argument("--version", action="version", version=f"prodc {__version__}")
41
+ parser.add_argument(
42
+ "-C",
43
+ "--chdir",
44
+ metavar="DIR",
45
+ default=None,
46
+ help="run as if prodc was started in DIR (like `terraform -chdir`); must precede the command",
47
+ )
48
+ sub = parser.add_subparsers(dest="command", required=True)
49
+ st = sub.add_parser("status", help="show the needs board")
50
+ st.add_argument("--config", default="prodc.toml", help="path to prodc.toml")
51
+ st.add_argument("--json", action="store_true", help="machine-readable output")
52
+ st.add_argument("--strict", action="store_true", help="also exit 1 on opinions")
53
+ st.add_argument("--wide", action="store_true", help="never truncate the need text")
54
+ st.add_argument(
55
+ "--client",
56
+ default=None,
57
+ metavar="ID",
58
+ help="scope the board + coverage to one client (its variants + baselines)",
59
+ )
60
+ st.add_argument(
61
+ "--goal",
62
+ default=None,
63
+ metavar="ID",
64
+ help="scope the board + coverage to the needs serving one goal",
65
+ )
66
+ st.add_argument(
67
+ "--columns",
68
+ default=None,
69
+ type=lambda s: [c.strip() for c in s.split(",")],
70
+ metavar="COLS",
71
+ help="flat table of just these columns, e.g. id,state,text,warrant",
72
+ )
73
+ st.add_argument("--only-must", action="store_true", help="show only must-have needs")
74
+ st.add_argument(
75
+ "--include-drafts",
76
+ action="store_true",
77
+ help="include draft (staged) needs in the board and coverage (#60)",
78
+ )
79
+ st.add_argument(
80
+ "--stale",
81
+ type=int,
82
+ default=None,
83
+ metavar="DAYS",
84
+ help="show only needs whose newest evidence (observed_at) is older than DAYS (#58)",
85
+ )
86
+ st.add_argument(
87
+ "--min-coverage",
88
+ type=float,
89
+ default=None,
90
+ metavar="FRAC",
91
+ help="gate: exit 1 if delivered/total < FRAC (0..1)",
92
+ )
93
+ st.add_argument(
94
+ "--max-dangling",
95
+ type=int,
96
+ default=None,
97
+ metavar="N",
98
+ help="gate: tolerate up to N broken locators instead of failing on any",
99
+ )
100
+ st.add_argument(
101
+ "--max-orphans",
102
+ type=int,
103
+ default=None,
104
+ metavar="N",
105
+ help="gate: exit 1 if more than N proof scenarios are claimed by no need",
106
+ )
107
+ st.set_defaults(func=cmd_status)
108
+
109
+ ln = sub.add_parser("lint", help="gate the board: fail on BROKEN proofs and must-opinions")
110
+ ln.add_argument("--config", default="prodc.toml", help="path to prodc.toml")
111
+ ln.add_argument("--json", action="store_true", help="machine-readable output")
112
+ ln.add_argument("--strict", action="store_true", help="also fail on any hygiene warning")
113
+ ln.set_defaults(func=cmd_lint)
114
+
115
+ sh = sub.add_parser("show", help="show one need as a citable answer")
116
+ sh.add_argument("need_id", help="the need id to show, e.g. N-1")
117
+ sh.add_argument("--config", default="prodc.toml", help="path to prodc.toml")
118
+ sh.add_argument("--json", action="store_true", help="machine-readable output")
119
+ sh.set_defaults(func=cmd_show)
120
+
121
+ br = sub.add_parser("brief", help="pre-meeting agenda for one client: open opinions and HOLEs")
122
+ br.add_argument("client", help="the client/variant id to brief, e.g. acme")
123
+ br.add_argument("--config", default="prodc.toml", help="path to prodc.toml")
124
+ br.add_argument("--json", action="store_true", help="machine-readable output")
125
+ br.set_defaults(func=cmd_brief)
126
+
127
+ pl = sub.add_parser("plan", help="ranked backlog from must + state + evidence (+ importance)")
128
+ pl.add_argument("--config", default="prodc.toml", help="path to prodc.toml")
129
+ pl.add_argument("--json", action="store_true", help="machine-readable output")
130
+ pl.add_argument(
131
+ "--include-drafts", action="store_true", help="include draft (staged) needs (#60)"
132
+ )
133
+ pl.set_defaults(func=cmd_plan)
134
+
135
+ ar = sub.add_parser("argument", help="show the GSN assurance graph and its defects")
136
+ ar.add_argument("--config", default="prodc.toml", help="path to prodc.toml")
137
+ ar.add_argument("--json", action="store_true", help="machine-readable output")
138
+ ar.add_argument("--strict", action="store_true", help="exit 1 if the graph has any defect")
139
+ ar.set_defaults(func=cmd_argument)
140
+
141
+ st2 = sub.add_parser("set", help="set or append one field on a need (writes YAML)")
142
+ st2.add_argument("need_id", help="the need id to edit, e.g. N-1")
143
+ st2.add_argument("assignment", help="key=value, or key+=value to append (proofs/children)")
144
+ st2.add_argument("--config", default="prodc.toml", help="path to prodc.toml")
145
+ st2.add_argument("--json", action="store_true", help="machine-readable output")
146
+ st2.add_argument(
147
+ "--dry-run", action="store_true", help="validate and show the diff, don't write"
148
+ )
149
+ st2.add_argument(
150
+ "--expect-sha",
151
+ default=None,
152
+ metavar="SHA256",
153
+ help="refuse if the file's content hash differs (optimistic lock)",
154
+ )
155
+ st2.set_defaults(func=cmd_set)
156
+
157
+ ad = sub.add_parser(
158
+ "add", help="create a new agent-authored need, or declare a role (writes YAML)"
159
+ )
160
+ ad.add_argument("noun", choices=["need", "role"], help="what to add (need or role)")
161
+ ad.add_argument("need_id", help="the new need's id (or role id for `add role`), e.g. N-9")
162
+ ad.add_argument("--text", required=True, help="the need text (or the role description)")
163
+ ad.add_argument(
164
+ "--who",
165
+ default=None,
166
+ help="need only: the author (recorded on the provenance source); required for `add need`",
167
+ )
168
+ ad.add_argument(
169
+ "--kind",
170
+ choices=["assumption", "secondary", "internal", "proxy"],
171
+ default="assumption",
172
+ help="need only: provenance source kind — assumption (default) or secondary; never "
173
+ "primary for an agent (internal/proxy accepted as aliases for assumption)",
174
+ )
175
+ ad.add_argument(
176
+ "--file",
177
+ required=True,
178
+ metavar="PATH",
179
+ help="prodc file to write into, relative to the config (must match a glob)",
180
+ )
181
+ ad.add_argument("--config", default="prodc.toml", help="path to prodc.toml")
182
+ ad.add_argument("--json", action="store_true", help="machine-readable output")
183
+ ad.add_argument(
184
+ "--strict-dupes",
185
+ action="store_true",
186
+ help="refuse to add if a possible duplicate exists (#55)",
187
+ )
188
+ ad.add_argument(
189
+ "--no-dupe-check", action="store_true", help="skip the duplicate check entirely (#55)"
190
+ )
191
+ ad.add_argument(
192
+ "--draft",
193
+ action="store_true",
194
+ help="stage as a draft: loads but stays off the counted board until promoted (#60)",
195
+ )
196
+ ad.add_argument(
197
+ "--dry-run", action="store_true", help="validate and show the diff, don't write"
198
+ )
199
+ ad.add_argument(
200
+ "--expect-sha",
201
+ default=None,
202
+ metavar="SHA256",
203
+ help="refuse if the file's content hash differs (optimistic lock)",
204
+ )
205
+ ad.set_defaults(func=cmd_add)
206
+
207
+ cp = sub.add_parser("capture", help="append a source (evidence) to an existing need")
208
+ cp.add_argument("need_id", help="the need id to add evidence to")
209
+ cp.add_argument(
210
+ "--kind",
211
+ required=True,
212
+ choices=[
213
+ "primary",
214
+ "secondary",
215
+ "assumption",
216
+ "interview",
217
+ "artifact",
218
+ "proxy",
219
+ "internal",
220
+ ],
221
+ help="source kind, strongest first: primary > secondary > assumption "
222
+ "(interview/artifact/proxy/internal accepted as aliases)",
223
+ )
224
+ cp.add_argument("--who", default=None, help="the person the evidence came from")
225
+ cp.add_argument("--date", default=None, help="when (ISO, e.g. 2026-10-05)")
226
+ cp.add_argument("--quote", default=None, help="a verbatim quote (for a primary source)")
227
+ cp.add_argument("--locator", default=None, help="a source locator (for a secondary source)")
228
+ cp.add_argument(
229
+ "--observed-at",
230
+ default=None,
231
+ metavar="ISO",
232
+ help="when this evidence was recorded/refreshed (default: today) (#58)",
233
+ )
234
+ cp.add_argument("--config", default="prodc.toml", help="path to prodc.toml")
235
+ cp.add_argument("--json", action="store_true", help="machine-readable output")
236
+ cp.add_argument(
237
+ "--dry-run", action="store_true", help="validate and show the diff, don't write"
238
+ )
239
+ cp.add_argument(
240
+ "--expect-sha",
241
+ default=None,
242
+ metavar="SHA256",
243
+ help="refuse if the file's content hash differs (optimistic lock)",
244
+ )
245
+ cp.set_defaults(func=cmd_capture)
246
+
247
+ at = sub.add_parser("attest", help="record a human or AI review of a need's warrant (#13)")
248
+ at.add_argument("need_id", help="the need id being attested")
249
+ at.add_argument("--by", required=True, help="who reviewed it — a person's name or a model id")
250
+ at.add_argument("--date", required=True, help="when (ISO, e.g. 2026-10-05)")
251
+ at.add_argument("--ai", action="store_true", help="the reviewer is an AI model, not a human")
252
+ at.add_argument(
253
+ "--verdict", default=None, help="the reviewer's conclusion, e.g. pass/concerns/fail"
254
+ )
255
+ at.add_argument("--comment", default=None, help="the review's feedback, kept on the record")
256
+ at.add_argument("--config", default="prodc.toml", help="path to prodc.toml")
257
+ at.add_argument("--json", action="store_true", help="machine-readable output")
258
+ at.add_argument(
259
+ "--dry-run", action="store_true", help="validate and show the diff, don't write"
260
+ )
261
+ at.add_argument(
262
+ "--expect-sha",
263
+ default=None,
264
+ metavar="SHA256",
265
+ help="refuse if the file's content hash differs (optimistic lock)",
266
+ )
267
+ at.set_defaults(func=cmd_attest)
268
+
269
+ ac = sub.add_parser("accept", help="record a human acceptance pass (accept proof) on a need")
270
+ ac.add_argument("need_id", help="the need id that was accepted")
271
+ ac.add_argument("--who", required=True, help="who accepted it")
272
+ ac.add_argument("--date", required=True, help="when (ISO, e.g. 2026-10-05)")
273
+ ac.add_argument("--revoke", action="store_true", help="remove this accept proof instead")
274
+ ac.add_argument("--config", default="prodc.toml", help="path to prodc.toml")
275
+ ac.add_argument("--json", action="store_true", help="machine-readable output")
276
+ ac.add_argument(
277
+ "--dry-run", action="store_true", help="validate and show the diff, don't write"
278
+ )
279
+ ac.add_argument(
280
+ "--expect-sha",
281
+ default=None,
282
+ metavar="SHA256",
283
+ help="refuse if the file's content hash differs (optimistic lock)",
284
+ )
285
+ ac.set_defaults(func=cmd_accept)
286
+
287
+ pf = sub.add_parser("portfolio", help="roll up many repos into one scoreboard")
288
+ pf.add_argument("configs", nargs="+", metavar="PRODC_TOML", help="one or more prodc.toml paths")
289
+ pf.add_argument("--json", action="store_true", help="machine-readable output")
290
+ pf.set_defaults(func=cmd_portfolio)
291
+
292
+ rc = sub.add_parser(
293
+ "recurrence", help="baselines shared across clients; --suggest proposes unlinked groups"
294
+ )
295
+ rc.add_argument("--config", default="prodc.toml", help="path to prodc.toml")
296
+ rc.add_argument("--json", action="store_true", help="machine-readable output")
297
+ rc.add_argument(
298
+ "--suggest",
299
+ action="store_true",
300
+ help="propose unlinked cross-client needs that could share a baseline (#56)",
301
+ )
302
+ rc.add_argument(
303
+ "--threshold",
304
+ type=float,
305
+ default=None,
306
+ metavar="FRAC",
307
+ help="similarity for --suggest (0..1); overrides [lint].duplicate_threshold",
308
+ )
309
+ rc.add_argument(
310
+ "--apply",
311
+ action="store_true",
312
+ help="with --suggest: link each group to a shared baseline (reuses a client-less "
313
+ "member, or creates one with --baseline-id/--file) (#56)",
314
+ )
315
+ rc.add_argument(
316
+ "--baseline-id",
317
+ default=None,
318
+ metavar="ID",
319
+ help="id for a baseline to create when a group has no client-less member",
320
+ )
321
+ rc.add_argument(
322
+ "--file",
323
+ default=None,
324
+ metavar="PATH",
325
+ help="file to write a created baseline into (must match a [prodc].needs glob)",
326
+ )
327
+ rc.add_argument(
328
+ "--dry-run", action="store_true", help="with --apply: validate and show diffs, don't write"
329
+ )
330
+ rc.set_defaults(func=cmd_recurrence)
331
+
332
+ sg = sub.add_parser(
333
+ "suggest", help="propose candidate proofs for HOLE needs from adapter inventories (#54)"
334
+ )
335
+ sg.add_argument("need_id", nargs="?", default=None, help="only this need (default: all HOLEs)")
336
+ sg.add_argument("--config", default="prodc.toml", help="path to prodc.toml")
337
+ sg.add_argument("--json", action="store_true", help="machine-readable output")
338
+ sg.add_argument(
339
+ "--min-score",
340
+ type=float,
341
+ default=0.3,
342
+ metavar="FRAC",
343
+ help="minimum text similarity to suggest (0..1, default 0.3)",
344
+ )
345
+ sg.add_argument("--top", type=int, default=3, metavar="N", help="max suggestions per need")
346
+ sg.add_argument(
347
+ "--kind", default=None, metavar="ADAPTER", help="only suggest from this adapter name"
348
+ )
349
+ sg.add_argument(
350
+ "--apply",
351
+ action="store_true",
352
+ help="write the best suggestion per need as a proof (#54); honours --min-score",
353
+ )
354
+ sg.add_argument(
355
+ "--dry-run",
356
+ action="store_true",
357
+ help="with --apply: validate and show the diff, don't write",
358
+ )
359
+ sg.set_defaults(func=cmd_suggest)
360
+
361
+ du = sub.add_parser("dupes", help="list candidate duplicate need pairs by text similarity")
362
+ du.add_argument("--config", default="prodc.toml", help="path to prodc.toml")
363
+ du.add_argument("--json", action="store_true", help="machine-readable output")
364
+ du.add_argument(
365
+ "--threshold",
366
+ type=float,
367
+ default=None,
368
+ metavar="FRAC",
369
+ help="similarity to flag (0..1); overrides [lint].duplicate_threshold",
370
+ )
371
+ du.set_defaults(func=cmd_dupes)
372
+
373
+ rp = sub.add_parser(
374
+ "report", help="render the board as a shareable document (markdown, or an interactive HTML)"
375
+ )
376
+ rp.add_argument("--config", default="prodc.toml", help="path to prodc.toml")
377
+ rp.add_argument(
378
+ "--format",
379
+ choices=["md", "html"],
380
+ default="md",
381
+ help="md (default) or html — a self-contained, interactive, read-only viewer (#79)",
382
+ )
383
+ rp.add_argument(
384
+ "--output",
385
+ "-o",
386
+ default=None,
387
+ metavar="PATH",
388
+ help="write the report to PATH (e.g. status.md / status.html) instead of stdout",
389
+ )
390
+ rp.add_argument(
391
+ "--client", default=None, metavar="ID", help="scope the report to one client (as `status`)"
392
+ )
393
+ rp.set_defaults(func=cmd_report)
394
+
395
+ sy = sub.add_parser(
396
+ "sync",
397
+ help="run lifecycle callbacks for needs whose state changed since the last sync (#71)",
398
+ )
399
+ sy.add_argument("--config", default="prodc.toml", help="path to prodc.toml")
400
+ sy.add_argument(
401
+ "--dry-run",
402
+ action="store_true",
403
+ help="show which transitions would fire callbacks; don't run them or write the ledger",
404
+ )
405
+ sy.set_defaults(func=cmd_sync)
406
+
407
+ ag = sub.add_parser(
408
+ "agent",
409
+ help="print prodc's agent playbook (teach a coding agent to run prodc as the project's PM)",
410
+ )
411
+ ag.add_argument(
412
+ "--write",
413
+ default=None,
414
+ metavar="PATH",
415
+ help="write the playbook to PATH instead of stdout (e.g. AGENTS.md)",
416
+ )
417
+ ag.add_argument("--force", action="store_true", help="overwrite PATH if it already exists")
418
+ ag.set_defaults(func=cmd_agent)
419
+
420
+ df = sub.add_parser(
421
+ "diff", help="diff the current board against a prior status --json snapshot"
422
+ )
423
+ df.add_argument("snapshot", help="path to a saved `prodc status --json` file")
424
+ df.add_argument("--config", default="prodc.toml", help="path to prodc.toml")
425
+ df.add_argument("--json", action="store_true", help="machine-readable output")
426
+ df.set_defaults(func=cmd_diff)
427
+
428
+ args = parser.parse_args(argv)
429
+ if args.chdir is not None:
430
+ try:
431
+ os.chdir(args.chdir)
432
+ except OSError as exc:
433
+ print(f"prodc: cannot chdir to {args.chdir}: {exc}", file=sys.stderr)
434
+ return 2
435
+ return int(args.func(args))
436
+
437
+
438
+ if __name__ == "__main__":
439
+ raise SystemExit(main())