jg-plans 0.0.0-stage → 0.1.2

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 (66) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE +9 -0
  4. package/README.md +50 -2
  5. package/api/README.md +96 -0
  6. package/cli/README.md +85 -0
  7. package/dist/cli/arguments.d.ts +7 -0
  8. package/dist/cli/arguments.js +66 -0
  9. package/dist/cli/cli.d.ts +2 -0
  10. package/dist/cli/cli.js +3 -0
  11. package/dist/cli/commands.d.ts +2 -0
  12. package/dist/cli/commands.js +201 -0
  13. package/dist/cli/types.d.ts +11 -0
  14. package/dist/cli/types.js +1 -0
  15. package/dist/documents/check.d.ts +10 -0
  16. package/dist/documents/check.js +26 -0
  17. package/dist/documents/index.d.ts +5 -0
  18. package/dist/documents/index.js +5 -0
  19. package/dist/documents/milestones.d.ts +10 -0
  20. package/dist/documents/milestones.js +190 -0
  21. package/dist/documents/plan.d.ts +10 -0
  22. package/dist/documents/plan.js +94 -0
  23. package/dist/documents/types.d.ts +63 -0
  24. package/dist/documents/types.js +1 -0
  25. package/dist/documents/utilities.d.ts +7 -0
  26. package/dist/documents/utilities.js +53 -0
  27. package/dist/edits/index.d.ts +1 -0
  28. package/dist/edits/index.js +2 -0
  29. package/dist/edits/types.d.ts +6 -0
  30. package/dist/edits/types.js +1 -0
  31. package/dist/edits/utilities.d.ts +35 -0
  32. package/dist/edits/utilities.js +241 -0
  33. package/dist/errors/constants.d.ts +11 -0
  34. package/dist/errors/constants.js +34 -0
  35. package/dist/errors/index.d.ts +1 -0
  36. package/dist/errors/index.js +1 -0
  37. package/dist/files/index.d.ts +4 -0
  38. package/dist/files/index.js +2 -0
  39. package/dist/files/mutations.d.ts +27 -0
  40. package/dist/files/mutations.js +242 -0
  41. package/dist/files/types.d.ts +35 -0
  42. package/dist/files/types.js +1 -0
  43. package/dist/files/utilities.d.ts +13 -0
  44. package/dist/files/utilities.js +103 -0
  45. package/dist/index.d.ts +11 -0
  46. package/dist/index.js +6 -0
  47. package/dist/protocol/constants.d.ts +4 -0
  48. package/dist/protocol/constants.js +4 -0
  49. package/dist/protocol/index.d.ts +5 -0
  50. package/dist/protocol/index.js +3 -0
  51. package/dist/protocol/types.d.ts +99 -0
  52. package/dist/protocol/types.js +78 -0
  53. package/docs/getting-started.md +36 -0
  54. package/docs/integrations.md +49 -0
  55. package/errors/README.md +29 -0
  56. package/examples/README.md +19 -0
  57. package/examples/extensions/plan.md +18 -0
  58. package/examples/incomplete/plan.md +9 -0
  59. package/examples/library.mjs +30 -0
  60. package/examples/orphan/milestones.md +8 -0
  61. package/examples/small/plan.md +19 -0
  62. package/examples/with-milestones/milestones.md +20 -0
  63. package/examples/with-milestones/plan.md +24 -0
  64. package/package.json +90 -3
  65. package/protocol/0.1/README.md +116 -0
  66. package/protocol/README.md +13 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,20 @@
1
+ # Changelog
2
+
3
+ ## 0.1.2 — documentation and examples
4
+
5
+ - Complete the canonical documentation and consumer integration recipes alongside the visual example explorer, original-source downloads, and verified package availability presentation.
6
+ - Keep protocol 0.1 provisional and the reference library/CLI behavior unchanged.
7
+
8
+ ## 0.1.1 — trusted publishing validation
9
+
10
+ - Prepare the first automated release after the manual 0.1.0 bootstrap, using the qualified archive and registry/provenance verification workflow.
11
+ - Keep protocol 0.1 and the reference API/CLI behavior from the initial preview.
12
+
13
+ ## 0.1.0 — development preview
14
+
15
+ - Introduce provisional protocol 0.1 for Markdown plans with optional milestones, extensible metadata, and ordinary user edits/deletion.
16
+ - Add tolerant parsing, selected-directory inspection, and optional conformance checking with structured diagnostics.
17
+ - Add exclusive plan creation and conservative metadata/marker updates that preserve unrelated bytes; document caller serialization and unsupported edits.
18
+ - Ship the library declarations, four-command CLI, canonical public references, synthetic examples, and a runnable installed-consumer example.
19
+ - Qualify an exact archive across the supported Node/OS matrix, with checksum receipts, registry/content checks, OIDC publishing readiness, and an isolated owner bootstrap runbook.
20
+ - Establish the private documentation website workspace and shared project maintenance skill. Public release and live deployment remain unverified.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jesus Graterol
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/NOTICE ADDED
@@ -0,0 +1,9 @@
1
+ The fenced-code and optional overview recognition in src/documents/ derives
2
+ from the public jg-agent-hq/src/plans/utilities.ts implementation:
3
+ https://github.com/jesusgraterol/jg-agent-hq
4
+
5
+ Copyright (c) 2026 Jesus Graterol. MIT License.
6
+ The full license is included in LICENSE.
7
+
8
+ Only reusable document mechanics are adapted. Consumer approvals, digests,
9
+ authorization, coordination, and project-specific metadata are not included.
package/README.md CHANGED
@@ -1,3 +1,51 @@
1
- # Temporary Holding Version
1
+ # jg-plans
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A compact planning protocol for people and coding agents. Keep the plan in Markdown, add milestones when they help, and let projects choose their workflows.
4
+
5
+ Protocol 0.1 is provisional. The [website](https://jg-plans.jesusgraterol.dev) presents the specification, documentation, and examples, with package availability verified independently of the protocol draft.
6
+
7
+ - [Protocol 0.1](protocol/0.1/README.md)
8
+ - [Examples](examples/README.md)
9
+ - [Getting started](docs/getting-started.md)
10
+ - [Library API](api/README.md) and [CLI](cli/README.md)
11
+ - [Consumer integration](docs/integrations.md)
12
+ - [Development](https://github.com/jesusgraterol/jg-plans/blob/main/docs/development.md)
13
+ - [Website development and visual checks](https://github.com/jesusgraterol/jg-plans/blob/main/website/README.md)
14
+
15
+ ## Project blueprint
16
+
17
+ | Area | Ownership |
18
+ | ------------------------------------ | -------------------------------------------------------------------------------------------------------- |
19
+ | protocol/ | Normative versioned specification and version index. |
20
+ | examples/ | Invented public documents and independently described expected observations. |
21
+ | docs/ | Concise durable concepts and operating guides; API and HTTP/error references stay outside docs/. |
22
+ | Root package | Public reference library and thin CLI. |
23
+ | src/ | Protocol schemas, tolerant parsers, selected-directory reads, targeted metadata/marker edits, and CLI. |
24
+ | api/, cli/, errors/ | Canonical public API, automation, and error references. |
25
+ | scripts/package/ | Build, local archive creation, and isolated installed-consumer verification. |
26
+ | scripts/release/, .github/workflows/ | Source/checksum receipts, registry/provenance verification, and qualified delivery. |
27
+ | website/ | Private Astro/Tailwind workspace for canonical docs, parsed examples, and verified release presentation. |
28
+ | .agents/skills/jg-plans-maintenance/ | Shared project maintenance skill; Claude discovers the same source through its repository symlink. |
29
+
30
+ Users own their documents and can edit or delete them. No history store, metadata field, CLI, or website is required to read Markdown. Optional conformance checks do not authorize execution or become a prerequisite to inspection.
31
+
32
+ The protocol stays separate from application schemas, approvals, resource allocations, locking and reconciliation. Future jg-skills and HQ adoption are independent integrations.
33
+
34
+ ## Development
35
+
36
+ Use Node.js 24.15.0 or newer and pnpm 12.14.0.
37
+
38
+ ```sh
39
+ pnpm install --frozen-lockfile
40
+ pnpm format:check
41
+ pnpm check:package
42
+ pnpm check
43
+ ```
44
+
45
+ The [development guide](https://github.com/jesusgraterol/jg-plans/blob/main/docs/development.md) owns checks and package/site boundaries. Root tests build first, exercise public compiled exports, and install the actual tarball in an isolated consumer. Website tests verify production output in both themes and narrow layouts.
46
+
47
+ The [runnable library example](examples/library.mjs) creates, inspects, updates, and checks documents in an owned temporary directory. Mutations preserve unrelated bytes and require callers to serialize edits to each document; unsupported edits fail without rewriting it. The [API](api/README.md) describes the exact preservation and concurrency limits.
48
+
49
+ ## License
50
+
51
+ [MIT](LICENSE), copyright Jesus Graterol.
package/api/README.md ADDED
@@ -0,0 +1,96 @@
1
+ # Library API
2
+
3
+ Protocol 0.1 is provisional. These operations describe the implemented reference library. Check the [website's availability label](https://jg-plans.jesusgraterol.dev/docs/api/) before choosing a published package version.
4
+
5
+ The ESM root exports compiled JavaScript and TypeScript declarations. Node.js 24.15.0 is the runtime floor. Importing jg-plans does not inspect files, log output, or start a process. The only public import path is `jg-plans`.
6
+
7
+ ```ts
8
+ import { PROTOCOL_VERSION, parsePlan, parseMilestones, checkPlan, inspectPlan } from 'jg-plans';
9
+
10
+ const plan = parsePlan('# Useful legacy Markdown');
11
+ const milestones = parseMilestones('## Milestone 1: Work\n**Status:** pending');
12
+ const check = checkPlan({ plan: plan.source, milestones: milestones.source });
13
+ const observation = await inspectPlan('./jg-plans/example', { maxBytes: 1_048_576 });
14
+ ```
15
+
16
+ ## Parsing and checking
17
+
18
+ `parsePlan(source)` retains the exact source and body. Its `metadata` contains independently validated core fields. `rawMetadata` retains the expanded YAML value, including unknown fields and invalid known values. Missing metadata, malformed YAML, unsupported expansion, and incompatible values receive structured diagnostics; they do not discard the body or authorize work.
19
+
20
+ YAML uses maxAliasCount: 100 for expansion protection. Recursive aliases, unsafe integer expansions, and values JSON cannot represent receive a `YAML_VALUE_UNAVAILABLE` warning; rawMetadata is unavailable (`null`) and the original YAML remains in source. Raw observations use YAML's JavaScript conversion; source remains authoritative for original numeric spelling and presentation. Independently readable core fields are still observed. An unterminated frontmatter block retains the complete source as body because its boundary is unknown.
21
+
22
+ `parseMilestones(source)` returns source, recognizable milestones, optional overview rows, overviewPresent, and diagnostics. overviewPresent distinguishes an absent table from an explicitly empty derived table; an existing table that omits observed milestones receives discrepancies. Each entry contains ordinal, title, observed status/ID or null, location, end offset, body, and all recognized status/ID markers. Duplicate markers remain visible and are diagnosed; the first observed marker supplies the convenience status/ID value. Consumers must consider ambiguity diagnostics before acting on it. Fenced examples are ignored. Missing IDs and overview tables are allowed. Gaps are observations, with no reconstructed entries or inferred completion.
23
+
24
+ `checkPlan({ plan?, milestones? })` checks explicitly supplied strings against preferred writer conformance. Omit absent or deleted documents. It returns `{ conforms, diagnostics }`; error diagnostics make conforms false. It never writes, restores data, or runs as a prerequisite to another operation.
25
+
26
+ ## Selected-directory inspection
27
+
28
+ `inspectPlan(directory, { maxBytes? }?)` reads only plan.md and milestones.md from that exact directory, concurrently and with a default 1 MiB limit per file. Explicit custom paths and coding-agent-planning/ work as supplied; there is no repository/root/history discovery.
29
+
30
+ The result includes the resolved directory, each document's path and readable/missing/unreadable state, useful parsed data for readable files, and aggregated diagnostics. A readable empty file differs from a missing file. Orphan milestones remain observable when the plan has been deleted. No predecessor is required or restored.
31
+
32
+ Bytes must decode strictly as UTF-8. An existing BOM is retained in source. Invalid bytes receive INVALID_ENCODING, and another readable document still contributes observations. File-size failures and nonregular/unreadable targets receive operational diagnostics. Final-component file symlinks are not followed; the selected directory's ancestors are caller-chosen. The library does not silently transcode content.
33
+
34
+ ## Types and schemas
35
+
36
+ Creation and update contracts are described below. A [runnable library example](../examples/library.mjs) exercises creation, sequential updates, inspection, and checking in an owned temporary directory.
37
+
38
+ Named public types are IPlanMetadata, IDiagnostic, ISourceLocation, IReadOptions, IPlanDocuments, IParsedPlan, IFrontmatter, IMarker, IMilestone, IOverviewRow, IParsedMilestones, ICheckResult, IDocumentRead, IPlanInspection, ICreatePlanOptions, IPlanCreation, IPlanUpdate, and IPlanUpdateResult.
39
+
40
+ PlanMetadataSchema defines preferred complete metadata and allows project extensions. DiagnosticSchema, SourceLocationSchema, ReadOptionsSchema, and PlanDocumentsSchema own the corresponding boundary contracts. Known fields are validated independently during tolerant reads. PROTOCOL_VERSION is `0.1`; it is independent of the npm package version.
41
+
42
+ Source offsets count JavaScript UTF-16 code units. Lines and columns start at one. Marker value spans exclude surrounding whitespace and line endings. Entries and overview rows retain ordinalText; ordinal is null for nonpositive or unsafe integer text, avoiding rounded identities. These are observations, not general-purpose rewrite permission.
43
+
44
+ ## Exclusive creation
45
+
46
+ ```ts
47
+ import { createPlan, updatePlan } from 'jg-plans';
48
+
49
+ const created = await createPlan({
50
+ root: './jg-plans', // optional; resolved from the current working directory
51
+ slug: 'small-change',
52
+ body: '# Intent\n\nKeep this prose.\n',
53
+ metadata: { project: { owner: 'example' } },
54
+ milestones: '## Milestone 1: Implement\n**Status:** pending\n', // optional, verbatim
55
+ });
56
+ await updatePlan(created.directory, { document: 'plan', set: { revision: 2 } });
57
+ ```
58
+
59
+ `createPlan(options)` creates `<unix-seconds>_<slug>/plan.md` exclusively beneath the supplied root, or ./jg-plans. It never discovers or migrates an existing root. Slugs use lowercase ASCII kebab case; the generated directory component must fit 64 characters. The generated relative file paths fit 160 characters. A supplied document ID is opaque and never becomes a path.
60
+
61
+ The closed CreatePlanOptionsSchema accepts slug, optional root, body, optional JSON metadata, and optional milestones text. Missing core metadata defaults to the generated name as id, one captured ISO creation time for created_at and updated_at, revision 1, and pending status. Supplied values override defaults, including nonconforming known values; diagnostics describe them without refusing to preserve them. Unknown project metadata is retained. The supplied Markdown body and milestone bytes are preserved; creation emits new UTF-8 frontmatter with LF separators.
62
+
63
+ The result contains directory, parsed plan, parsed milestones or null, and diagnostics. Existing destinations fail without overwriting. Failed creation attempts remove only recognized files created by that call and the empty owned plan directory. Intervening user content is preserved; incomplete cleanup is reported in error.data.cleanupFailedPaths. An empty selected storage root can remain after failure.
64
+
65
+ ## Targeted updates
66
+
67
+ `updatePlan(directory, patch, { maxBytes? }?)` edits one existing document and returns `{ directory, document, changed, diagnostics }`. The closed PlanUpdateSchema accepts either:
68
+
69
+ ```json
70
+ { "document": "plan", "set": { "status": "in progress", "revision": 2 } }
71
+ ```
72
+
73
+ ```json
74
+ {
75
+ "document": "milestones",
76
+ "changes": [{ "select": { "ordinal": 1 }, "set": { "status": "done" } }]
77
+ }
78
+ ```
79
+
80
+ PlanSetSchema allows only id, created_at, updated_at, revision, and status, validated by their preferred core schemas. Supply at least one field. Revision and updated_at change only when explicitly requested. MilestoneSelectorSchema selects one entry by UUID id or positive safe ordinal, never both. MilestoneSetSchema allows status and/or UUID id. Status must be nonblank, trimmed, and contain no CR/LF. Each target occurs at most once per call; combine its changes in one set.
81
+
82
+ The update reads only the selected document, bounded at 1 MiB by default. A malformed or deleted companion document does not block an independent edit. Missing targets are never recreated. Insertions require an unambiguous location: absent plan fields go before the closing frontmatter delimiter; metadata-free Markdown receives frontmatter containing only requested fields. New status markers go immediately after their heading, and new IDs immediately after an unambiguous canonical status line. Existing misplaced markers are updated in place and remain diagnosed; the overview table and prose are not changed.
83
+
84
+ Every byte outside the requested value spans or insertion points is preserved, including BOM, supplementary Unicode, unrelated nested metadata, comments, quotes, whitespace, line endings, and final-newline state. Equal values produce a no-op and skip replacement. Existing single quotes are retained when their style can represent the value; replacements otherwise use quoted YAML scalars.
85
+
86
+ Unsupported edits fail without writing. These include malformed/unterminated frontmatter, a flow-style root mapping, complex/anchored/tagged root keys, merge keys, duplicate affected keys, and affected aliases, anchors, tags, block/multiline scalars, or implicit empty nulls. Duplicate/missing milestone selectors, repeated targets, duplicate affected markers, and overlapping spans also fail. Unrelated nonconforming fields are not a conformance gate. error.data.reason identifies the unsupported case, with field when relevant; it contains no source snippet. This is a conservative scalar/marker editor, with no general YAML or Markdown rewrite API.
87
+
88
+ Bytes must be valid UTF-8, and replacement strings cannot contain unpaired UTF-16 surrogates. The caller-selected directory and its ancestors are trusted path choices; links below that root are rejected. Replacement preserves file permissions. **Serialize the entire read/edit/write operation for a document, including within one process.** The prior-content comparison detects observed intervening changes, but a comparison-to-rename race remains. No lock, queue, journal, snapshot service, or automatic retry is provided.
89
+
90
+ CreatePlanOptionsSchema and PlanUpdateSchema are public boundary schemas; PlanSetSchema, MilestoneSelectorSchema, and MilestoneSetSchema expose their constituent contracts. New metadata is JSON-compatible; original YAML source remains authoritative on reads.
91
+
92
+ ## Failures
93
+
94
+ Invalid operation arguments throw JG_PLANS_INVALID_INPUT: Invalid jg-plans input. File absence, formatting defects, encoding failures, and read failures are returned as inspection diagnostics. Mutations throw operational errors for unsupported text/edits, existing destinations, detected source changes, and read/write failures. See the [error reference](../errors/README.md) for exact codes/messages and per-operation mapping. ERRORS exposes the stable operational codes.
95
+
96
+ Approval, authorization, stricter profiles, coordination, retention, and execution remain consumer-owned. See the [protocol](../protocol/0.1/README.md) and [integration guide](../docs/integrations.md).
package/cli/README.md ADDED
@@ -0,0 +1,85 @@
1
+ # CLI
2
+
3
+ The executable is tested from the packed archive. Check the [website's availability label](https://jg-plans.jesusgraterol.dev/docs/cli/) for published-package availability, or run the built executable from a checkout.
4
+
5
+ ```sh
6
+ jg-plans inspect --directory ./jg-plans/example
7
+ jg-plans inspect --directory ./custom-root/example --json
8
+ jg-plans inspect --directory ./coding-agent-planning/example --include-body --json
9
+ jg-plans check --directory ./jg-plans/example --json
10
+ jg-plans --help
11
+ jg-plans --version
12
+ ```
13
+
14
+ ## Inspect
15
+
16
+ Inspect reads only the selected plan.md and optional milestones.md. It reports readable, missing, or unreadable files, recognized metadata and progress, and useful partial observations. Unknown metadata and custom status strings remain visible. It does not read history, discover other plans, restore deleted files, or infer permission or completion from absence.
17
+
18
+ Bodies and complete document sources are excluded by default. `--include-body` explicitly includes them for inspect. Metadata, titles, markers, and locations remain part of ordinary observations.
19
+
20
+ ## Check
21
+
22
+ Check is an optional assessment against the preferred protocol writer format. Missing required metadata, malformed structures, duplicate markers, and overview discrepancies produce findings. Milestone IDs and overview tables remain optional. A missing milestone file is valid for a plan-only task. A checker result is never an execution approval or a restriction on manual editing.
23
+
24
+ ## Create and update
25
+
26
+ ```sh
27
+ jg-plans create --slug small-change --input create.json --json
28
+ jg-plans create --slug small-change --root ./custom-root --input - --json
29
+ jg-plans update --directory ./jg-plans/example --input update.json --json
30
+ ```
31
+
32
+ Create input is a closed JSON object with body, optional metadata, and optional milestones text:
33
+
34
+ ```json
35
+ {
36
+ "body": "# Intent\n\nKeep this prose.\n",
37
+ "metadata": { "project": { "owner": "example" } },
38
+ "milestones": "## Milestone 1: Implement\n**Status:** pending\n"
39
+ }
40
+ ```
41
+
42
+ Slug and root are command flags, not JSON fields. Creation uses ./jg-plans relative to the working directory by default, without root discovery or migration. It creates an exclusive timestamp_slug directory, defaults missing metadata, preserves supplied body/milestones, and never overwrites. Slugs use lowercase ASCII kebab case; the generated component must fit 64 characters. Returned create data contains directory, plan metadata/rawMetadata, and milestone summaries or null. No body is emitted.
43
+
44
+ Update input selects one document. For a plan, use `{ "document": "plan", "set": { "status": "done", "revision": 2 } }`. The only allowed fields are id, created_at, updated_at, revision, and status. For milestones:
45
+
46
+ ```json
47
+ {
48
+ "document": "milestones",
49
+ "changes": [{ "select": { "ordinal": 1 }, "set": { "status": "done" } }]
50
+ }
51
+ ```
52
+
53
+ Select by ordinal or UUID id, with one change per target. Set status and/or UUID id. The result contains directory, document, and changed. Equal values skip the write. Revision/timestamps never advance implicitly. Overview rows, prose, and extension metadata remain byte-identical. Missing selected files are not recreated. Existing misplaced markers remain in place with diagnostics; new markers require canonical insertion points.
54
+
55
+ Input comes from a file or `-` for stdin and must be valid UTF-8 JSON, at most 1 MiB by default. `--max-bytes` overrides both mutation-input and selected-document limits. Unknown JSON fields, nonconforming requested core values, invalid encoding, unsupported YAML, ambiguous edits, destination collisions, detected source changes, and I/O failures exit 2. Unsupported edits explain the case in error.data.reason; cleanup failures report owned paths. See [the API](../api/README.md) for precise preservation and unsupported-edit limits.
56
+
57
+ Serialize commands that edit the same document, including the whole read/edit/write operation. A prior-content check detects observed intervening changes, but the comparison-to-rename race remains. The CLI supplies no lock, queue, retry, or history service.
58
+
59
+ ## Limits and output
60
+
61
+ `--max-bytes <positive-safe-integer>` overrides the default 1048576-byte per-file bound. Inputs must be valid UTF-8; invalid bytes are diagnosed without replacement/transcoding. Help and version do not read selected plan files.
62
+
63
+ Human output uses stdout for data and stderr for diagnostics. `--json` emits exactly one JSON envelope on stdout and no human/progress messages:
64
+
65
+ ```json
66
+ {
67
+ "schemaVersion": 1,
68
+ "command": "inspect",
69
+ "data": {},
70
+ "diagnostics": [],
71
+ "error": null
72
+ }
73
+ ```
74
+
75
+ Inspect data includes the resolved directory and per-document observations; check also includes conforms. Mutation results are described above. Diagnostics have stable codes, severity, document, message, and an optional source location. Error is null or a safe code/message pair with optional data for a reason, affected core field, or cleanupFailedPaths. Causes are not emitted. Operational inspection failure can retain partial data. Source locations use UTF-16 character offsets and one-based lines/columns. The JSON contract is versioned independently of the protocol.
76
+
77
+ | Exit | Meaning |
78
+ | ---- | ----------------------------------------------------------------------------------------------------------------------------- |
79
+ | 0 | Inspection completed, including absence or document-format/encoding findings; a mutation succeeded; or check found no errors. |
80
+ | 1 | Explicit check found conformance errors. |
81
+ | 2 | Invalid command/arguments or operational failure, including unreadable nonregular files and exceeded byte limits. |
82
+
83
+ Unknown/repeated flags, unsupported positionals, empty option values, and unsafe byte limits fail rather than silently changing the command. `--include-body` applies only to inspect.
84
+
85
+ See the [library API](../api/README.md), [error reference](../errors/README.md), and [protocol](../protocol/0.1/README.md).
@@ -0,0 +1,7 @@
1
+ import type { IArguments } from './types.ts';
2
+ /**
3
+ * Validate a single CLI request before reading documents.
4
+ * @throws
5
+ * - JG_PLANS_INVALID_INPUT: Invalid jg-plans input
6
+ */
7
+ export declare const parseArguments: (args: string[]) => IArguments;
@@ -0,0 +1,66 @@
1
+ import { parseStrictArgs } from 'jg-process';
2
+ import { planErrors } from "../errors/index.js";
3
+ import { ReadOptionsSchema } from "../protocol/index.js";
4
+ /**
5
+ * Validate a single CLI request before reading documents.
6
+ * @throws
7
+ * - JG_PLANS_INVALID_INPUT: Invalid jg-plans input
8
+ */
9
+ export const parseArguments = (args) => {
10
+ const [first, ...tail] = args;
11
+ const isCommand = Boolean(first && !first.startsWith('-'));
12
+ let flags;
13
+ try {
14
+ flags = parseStrictArgs(isCommand ? tail : args, {
15
+ directory: { type: 'string' },
16
+ input: { type: 'string' },
17
+ slug: { type: 'string' },
18
+ root: { type: 'string' },
19
+ 'max-bytes': { type: 'string' },
20
+ 'include-body': { type: 'boolean' },
21
+ json: { type: 'boolean' },
22
+ help: { type: 'boolean', short: 'h' },
23
+ version: { type: 'boolean', short: 'v' },
24
+ });
25
+ }
26
+ catch (cause) {
27
+ // Argument errors may include supplied values; keep the public message stable.
28
+ throw planErrors.create('invalidInput', { cause });
29
+ }
30
+ if (flags.help && flags.version)
31
+ throw planErrors.create('invalidInput');
32
+ if (flags.help || args.length === 0)
33
+ return { command: 'help', json: flags.json, includeBody: false };
34
+ if (flags.version)
35
+ return { command: 'version', json: flags.json, includeBody: false };
36
+ if (first !== 'inspect' && first !== 'check' && first !== 'create' && first !== 'update')
37
+ throw planErrors.create('invalidInput');
38
+ if (first === 'create') {
39
+ if (!flags.slug || !flags.input || flags.directory || flags['include-body'])
40
+ throw planErrors.create('invalidInput');
41
+ }
42
+ else {
43
+ if (!flags.directory || flags.root || flags.slug)
44
+ throw planErrors.create('invalidInput');
45
+ if (first === 'update'
46
+ ? !flags.input || flags['include-body']
47
+ : Boolean(flags.input) || (first === 'check' && flags['include-body']))
48
+ throw planErrors.create('invalidInput');
49
+ }
50
+ const maximum = flags['max-bytes'];
51
+ if (maximum !== undefined && !/^[1-9]\d*$/.test(maximum))
52
+ throw planErrors.create('invalidInput');
53
+ const maxBytes = maximum === undefined ? undefined : Number(maximum);
54
+ if (!ReadOptionsSchema.safeParse({ maxBytes }).success)
55
+ throw planErrors.create('invalidInput');
56
+ return {
57
+ command: first,
58
+ ...(flags.directory ? { directory: flags.directory } : {}),
59
+ ...(flags.input ? { input: flags.input } : {}),
60
+ ...(flags.slug ? { slug: flags.slug } : {}),
61
+ ...(flags.root ? { root: flags.root } : {}),
62
+ includeBody: flags['include-body'],
63
+ json: flags.json,
64
+ ...(maxBytes === undefined ? {} : { maxBytes }),
65
+ };
66
+ };
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { runCli } from "./commands.js";
3
+ process.exitCode = await runCli(process.argv.slice(2));
@@ -0,0 +1,2 @@
1
+ /** CLI entry only: public package imports never load or run this module. */
2
+ export declare const runCli: (args: string[]) => Promise<number>;
@@ -0,0 +1,201 @@
1
+ import { join } from 'node:path';
2
+ import { getErrorCode, isException } from 'jg-exception';
3
+ import { readBoundedFile } from 'jg-fs';
4
+ import { readStdin } from 'jg-process';
5
+ import { z } from 'zod';
6
+ import { checkPlan } from "../documents/index.js";
7
+ import { decodeSource } from "../edits/index.js";
8
+ import { planErrors } from "../errors/index.js";
9
+ import { createPlan, inspectPlan, updatePlan } from "../files/index.js";
10
+ import { CreatePlanOptionsSchema, DEFAULT_MAX_BYTES, PlanUpdateSchema, } from "../protocol/index.js";
11
+ import { parseArguments } from "./arguments.js";
12
+ const HELP = `jg-plans — inspect Markdown plans without enforcing a workflow
13
+
14
+ Commands:
15
+ inspect --directory <path> [--include-body] [--json] [--max-bytes <bytes>]
16
+ check --directory <path> [--json] [--max-bytes <bytes>]
17
+ create --slug <slug> --input <file-or-dash> [--root <path>] [--json] [--max-bytes <bytes>]
18
+ update --directory <path> --input <file-or-dash> [--json] [--max-bytes <bytes>]
19
+ --help, -h
20
+ --version, -v
21
+
22
+ Only the selected plan.md and optional milestones.md are read.
23
+ The default bound is 1048576 bytes per file. Checks are optional.
24
+ Updates to the same document must run sequentially. No history is required.
25
+ Exit 0: completed inspection, successful mutation, or conforming check; 1: conformance errors;
26
+ 2: invalid command or operational failure. Missing files do not imply completion.`;
27
+ const projectInspection = (inspection, includeBody) => {
28
+ const plan = inspection.plan;
29
+ const milestones = inspection.milestones;
30
+ const planValue = plan.state === 'readable' ? plan.value : undefined;
31
+ const milestoneValue = milestones.state === 'readable' ? milestones.value : undefined;
32
+ return {
33
+ directory: inspection.directory,
34
+ plan: {
35
+ state: plan.state,
36
+ path: plan.path,
37
+ ...(planValue
38
+ ? {
39
+ metadata: planValue.metadata,
40
+ rawMetadata: planValue.rawMetadata,
41
+ frontmatter: planValue.frontmatter,
42
+ ...(includeBody ? { source: planValue.source, body: planValue.body } : {}),
43
+ }
44
+ : {}),
45
+ },
46
+ milestones: {
47
+ state: milestones.state,
48
+ path: milestones.path,
49
+ ...(milestoneValue
50
+ ? {
51
+ overview: milestoneValue.overview,
52
+ overviewPresent: milestoneValue.overviewPresent,
53
+ milestones: milestoneValue.milestones.map(({ body, ...entry }) => ({
54
+ ...entry,
55
+ ...(includeBody ? { body } : {}),
56
+ })),
57
+ ...(includeBody ? { source: milestoneValue.source } : {}),
58
+ }
59
+ : {}),
60
+ },
61
+ };
62
+ };
63
+ const readMutationInput = async (input, maxBytes) => {
64
+ let bytes;
65
+ try {
66
+ bytes = input === '-' ? await readStdin({ maxBytes }) : await readBoundedFile(input, maxBytes);
67
+ }
68
+ catch (cause) {
69
+ const reason = getErrorCode(cause);
70
+ throw planErrors.create('readFailed', { cause, ...(reason ? { data: { reason } } : {}) });
71
+ }
72
+ let text;
73
+ try {
74
+ text = decodeSource(bytes);
75
+ }
76
+ catch {
77
+ throw planErrors.create('unsupportedEncoding');
78
+ }
79
+ try {
80
+ return JSON.parse(text.replace(/^\uFEFF/, ''));
81
+ }
82
+ catch {
83
+ // JSON syntax errors may contain input snippets; keep those unsafe causes out of errors.
84
+ throw planErrors.create('invalidInput');
85
+ }
86
+ };
87
+ /** CLI entry only: public package imports never load or run this module. */
88
+ export const runCli = async (args) => {
89
+ let json = args.includes('--json');
90
+ let command = args[0] ?? 'help';
91
+ try {
92
+ const options = parseArguments(args);
93
+ json = options.json;
94
+ command = options.command;
95
+ let data;
96
+ let diagnostics = [];
97
+ let error = null;
98
+ let exitCode = 0;
99
+ if (options.command === 'help')
100
+ data = { help: HELP };
101
+ else if (options.command === 'version') {
102
+ const raw = JSON.parse((await readBoundedFile(join(import.meta.dirname, '../../package.json'), 1024 * 1024)).toString('utf8'));
103
+ const manifest = z.object({ version: z.string() }).safeParse(raw);
104
+ if (!manifest.success)
105
+ throw planErrors.create('readFailed');
106
+ data = { version: manifest.data.version };
107
+ }
108
+ else if (options.command === 'create') {
109
+ const raw = await readMutationInput(options.input, options.maxBytes ?? DEFAULT_MAX_BYTES);
110
+ const parsed = CreatePlanOptionsSchema.omit({ slug: true, root: true }).safeParse(raw);
111
+ if (!parsed.success)
112
+ throw planErrors.create('invalidInput');
113
+ const result = await createPlan({
114
+ ...parsed.data,
115
+ slug: options.slug,
116
+ ...(options.root ? { root: options.root } : {}),
117
+ });
118
+ diagnostics = result.diagnostics;
119
+ data = {
120
+ directory: result.directory,
121
+ plan: { metadata: result.plan.metadata, rawMetadata: result.plan.rawMetadata },
122
+ milestones: result.milestones?.milestones.map((entry) => ({
123
+ ordinal: entry.ordinal,
124
+ ordinalText: entry.ordinalText,
125
+ title: entry.title,
126
+ id: entry.id,
127
+ status: entry.status,
128
+ location: entry.location,
129
+ })) ?? null,
130
+ };
131
+ }
132
+ else if (options.command === 'update') {
133
+ const raw = await readMutationInput(options.input, options.maxBytes ?? DEFAULT_MAX_BYTES);
134
+ const parsed = PlanUpdateSchema.safeParse(raw);
135
+ if (!parsed.success)
136
+ throw planErrors.create('invalidInput');
137
+ const result = await updatePlan(options.directory, parsed.data, options.maxBytes === undefined ? {} : { maxBytes: options.maxBytes });
138
+ diagnostics = result.diagnostics;
139
+ data = { directory: result.directory, document: result.document, changed: result.changed };
140
+ }
141
+ else {
142
+ const inspection = await inspectPlan(options.directory, options.maxBytes === undefined ? {} : { maxBytes: options.maxBytes });
143
+ diagnostics = inspection.diagnostics;
144
+ const operationalFailure = diagnostics.some((item) => item.code === 'UNREADABLE_DOCUMENT' || item.code === 'FILE_TOO_LARGE');
145
+ if (options.command === 'check') {
146
+ if (inspection.plan.state !== 'unreadable' &&
147
+ inspection.milestones.state !== 'unreadable') {
148
+ diagnostics = checkPlan({
149
+ ...(inspection.plan.state === 'readable' ? { plan: inspection.plan.value.source } : {}),
150
+ ...(inspection.milestones.state === 'readable'
151
+ ? { milestones: inspection.milestones.value.source }
152
+ : {}),
153
+ }).diagnostics;
154
+ }
155
+ const conforms = !diagnostics.some((item) => item.severity === 'error');
156
+ data = { ...projectInspection(inspection, false), conforms };
157
+ exitCode = conforms ? 0 : 1;
158
+ }
159
+ else
160
+ data = projectInspection(inspection, options.includeBody);
161
+ if (operationalFailure) {
162
+ const failure = planErrors.create('readFailed');
163
+ error = { code: failure.code, message: failure.message };
164
+ exitCode = 2;
165
+ }
166
+ }
167
+ if (json)
168
+ console.log(JSON.stringify({ schemaVersion: 1, command, data, diagnostics, error }));
169
+ else {
170
+ if ('help' in data)
171
+ console.log(data.help);
172
+ else if ('version' in data)
173
+ console.log(data.version);
174
+ else
175
+ console.log(JSON.stringify(data, null, 2));
176
+ for (const item of diagnostics)
177
+ console.error(`${item.document}${item.location ? `:${item.location.line}:${item.location.column}` : ''} ${item.severity} ${item.code}: ${item.message}`);
178
+ if (error)
179
+ console.error(`${error.code}: ${error.message}`);
180
+ }
181
+ return exitCode;
182
+ }
183
+ catch (cause) {
184
+ const failure = isException(cause) && planErrors.is(cause)
185
+ ? cause
186
+ : planErrors.create('readFailed', { cause });
187
+ const error = {
188
+ code: failure.code,
189
+ message: failure.message,
190
+ ...(failure.data ? { data: failure.data } : {}),
191
+ };
192
+ if (json)
193
+ console.log(JSON.stringify({ schemaVersion: 1, command, data: null, diagnostics: [], error }));
194
+ else {
195
+ console.error(`${error.code}: ${error.message}. Run jg-plans --help.`);
196
+ if (failure.data)
197
+ console.error(`Details: ${JSON.stringify(failure.data)}`);
198
+ }
199
+ return 2;
200
+ }
201
+ };
@@ -0,0 +1,11 @@
1
+ /** Validated CLI requests; file paths stay explicit. */
2
+ export type IArguments = {
3
+ command: 'inspect' | 'check' | 'create' | 'update' | 'help' | 'version';
4
+ directory?: string;
5
+ maxBytes?: number;
6
+ input?: string;
7
+ slug?: string;
8
+ root?: string;
9
+ includeBody: boolean;
10
+ json: boolean;
11
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,10 @@
1
+ import { type IPlanDocuments } from '../protocol/index.ts';
2
+ import type { ICheckResult } from './types.ts';
3
+ /**
4
+ * Explicitly check preferred writer conformance. Inspection never requires this operation.
5
+ * @param documents Selected document sources; omit deleted or absent files.
6
+ * @returns Conformance and diagnostics. No source is changed or restored.
7
+ * @throws
8
+ * - JG_PLANS_INVALID_INPUT: Invalid jg-plans input
9
+ */
10
+ export declare const checkPlan: (documents: IPlanDocuments) => ICheckResult;
@@ -0,0 +1,26 @@
1
+ import { planErrors } from "../errors/index.js";
2
+ import { PlanDocumentsSchema } from "../protocol/index.js";
3
+ import { parseMilestones } from "./milestones.js";
4
+ import { parsePlan } from "./plan.js";
5
+ import { diagnostic } from "./utilities.js";
6
+ /**
7
+ * Explicitly check preferred writer conformance. Inspection never requires this operation.
8
+ * @param documents Selected document sources; omit deleted or absent files.
9
+ * @returns Conformance and diagnostics. No source is changed or restored.
10
+ * @throws
11
+ * - JG_PLANS_INVALID_INPUT: Invalid jg-plans input
12
+ */
13
+ export const checkPlan = (documents) => {
14
+ const parsed = PlanDocumentsSchema.safeParse(documents);
15
+ if (!parsed.success)
16
+ throw planErrors.create('invalidInput');
17
+ const diagnostics = parsed.data.plan === undefined
18
+ ? [diagnostic('plan', 'MISSING_PLAN', 'The plan file is absent.')]
19
+ : parsePlan(parsed.data.plan).diagnostics;
20
+ if (parsed.data.milestones !== undefined) {
21
+ diagnostics.push(...parseMilestones(parsed.data.milestones).diagnostics);
22
+ if (parsed.data.plan === undefined)
23
+ diagnostics.push(diagnostic('milestones', 'ORPHAN_MILESTONES', 'Milestones remain without a plan; no plan is restored.', undefined, 'warning'));
24
+ }
25
+ return { conforms: !diagnostics.some((item) => item.severity === 'error'), diagnostics };
26
+ };