Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .replworks/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,14 +83,15 @@ Task Ledger Reader and Unverified Mark Scanner → Document Line Classifier
- Responsibilities:
- Obtain the Canonical Text and Helper Script Text before any write.
- Make no write when required source text is `Failed`.
- Process items in this order: AGENTS.md, then the `.replworks/` directory, then `.replworks/PRODUCT_SPEC.md`, `.replworks/TECH_STACK.md`, `.replworks/ARCHITECTURE.md`, `.replworks/TASKS.md`, then the helper-script state.
- Mark an item `skipped` when it is present. Otherwise create it (AGENTS.md with the Canonical Text, the directory, and each document as an empty file).
- Process items in this order: AGENTS.md, then the `.replworks/` directory, the four authoritative documents, `.replworks/docs/`, `.replworks/docs/IDEAS.md`, `.replworks/docs/PITCHING_SCRIPT.md`, then the helper-script state.
- Mark an item `skipped` when it is present. Otherwise create it (AGENTS.md with the Canonical Text, directories, and each document as an empty file).
- Create the helper-script directory when absent, replace the helper script with Helper Script Text whether absent or present, and ensure it has execute permission.
- Never modify or delete the human-edited documents after creation; their paths are managed only through create-if-absent operations.
- Stop at the first write failure.
- Inputs: Request with the canonical document source, Canonical Text fact, Helper Script Text fact, and Workspace presence facts.
- Outputs: Outcome with one `(item path, created | skipped | replaced)` entry per processed item, and execution errors.
- Ownership boundaries:
- Owns the item order and the created/skipped decision.
- Owns the item order and the created/skipped/replaced decision.
- Does not perform I/O itself, print, or compare content.
- Holds the Workspace `Write` capability.

Expand Down
9 changes: 7 additions & 2 deletions .replworks/PRODUCT_SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
- Exit code of each command.
- AGENTS.md file and .replworks/ directory created by repl init.
- `.replworks/scripts/ask-human.sh` created or replaced by init and sync.
- `.replworks/docs/IDEAS.md` and `.replworks/docs/PITCHING_SCRIPT.md` created empty by init when absent.
- AGENTS.md file replaced by repl sync.

## Constraints
Expand All @@ -78,7 +79,7 @@
### Commands

- EL-001: repl init
- Purpose: create AGENTS.md, the .replworks/ directory, and the four authoritative documents under .replworks/ in the project directory.
- Purpose: create AGENTS.md, the .replworks/ directory, the four authoritative documents, and the two human-edited documents under .replworks/ in the project directory.
- Input: none.
- Output: one line per created, skipped, or replaced item, each containing the item path.
- Exit: 0 on completion, 2 on execution error.
Expand Down Expand Up @@ -118,13 +119,15 @@
- FR-001: repl init MUST create AGENTS.md with the content of `https://www.repl.net/documents/agents.md` when AGENTS.md does not exist, without prompting for a selection.
- FR-002: repl init MUST create the .replworks/ directory in the project directory when it does not exist.
- FR-003: repl init MUST NOT modify an existing file except that it MUST replace `.replworks/scripts/ask-human.sh` with the exact published response and ensure that it has execute permission.
- FR-004: repl init MUST NOT create any file or directory other than AGENTS.md, .replworks/, the four documents defined in FR-036, and `.replworks/scripts/ask-human.sh` plus its parent directory.
- FR-004: repl init MUST NOT create any file or directory other than AGENTS.md, .replworks/, the four documents defined in FR-036, `.replworks/scripts/ask-human.sh` plus its parent directory, and the files and directory defined in FR-043.
- FR-005: repl init MUST print the path of each item it created.
- FR-006: repl init MUST print the path of each item it skipped because the item already exists.
- FR-007: repl init MUST exit with code 0 when it completes, including when it skipped items.
- FR-036: repl init MUST create .replworks/PRODUCT_SPEC.md, .replworks/TECH_STACK.md, .replworks/ARCHITECTURE.md, and .replworks/TASKS.md as empty files when they do not exist.
- FR-037: repl init MUST create `.replworks/scripts/ask-human.sh` from the exact response text at `https://www.repl.net/scripts/ask-human.sh` when the file does not exist, and MUST replace the content of an existing file with that response while ensuring that it has execute permission.
- FR-038: repl init MUST NOT prompt for a canonical document selection.
- FR-043: repl init MUST create `.replworks/docs/` when it does not exist and create empty `.replworks/docs/IDEAS.md` and `.replworks/docs/PITCHING_SCRIPT.md` files when they do not exist. Existing entries MUST be skipped and left unchanged.
- FR-044: After either human-edited document exists, no repl-cli command MUST modify or delete it.

### status

Expand Down Expand Up @@ -261,3 +264,5 @@
- AC-038: Given an AGENTS.md that differs from the canonical document, when repl sync writes to a terminal, then removed difference lines contain the red terminal color sequence and added difference lines contain the green terminal color sequence. When output is not a terminal, the difference contains no color sequences. (FR-035, EL-005)
- AC-039: Given an empty directory, when repl init runs without input, then .replworks/PRODUCT_SPEC.md, .replworks/TECH_STACK.md, .replworks/ARCHITECTURE.md, and .replworks/TASKS.md exist with zero bytes, and each is reported as created. (FR-005, FR-036, EL-001)
- AC-040: Given a .replworks/ directory that contains some but not all of the four documents, when repl init runs, then each missing document is created empty, each existing document is unchanged and reported as skipped, and the exit code is 0. (FR-003, FR-006, FR-036, EL-001)
- AC-045: Given a project without `.replworks/docs/`, `repl init` creates the directory and empty `IDEAS.md` and `PITCHING_SCRIPT.md` files, and reports each created path.
- AC-046: Given either human-edited file already exists with content, `repl init` reports it as skipped and preserves its content byte for byte. `repl sync` also leaves both files unchanged.
9 changes: 9 additions & 0 deletions .replworks/TASKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -297,3 +297,12 @@
- Automatic-run sync adds execute permission to an existing helper script when AGENTS.md is replaced without changing its content.
- Sync with identical AGENTS.md does not modify the helper script.
- Unit and command-level tests pass.

- [X] T-028 Create human-edited docs files with init
- Satisfies: PRODUCT_SPEC.md § Functional Requirements / init (FR-043, FR-044); § Acceptance Criteria (AC-045, AC-046); ARCHITECTURE.md § Components (C2)
- External boundary: yes
- Acceptance criteria:
- An E2E test exercises init through the live canonical sources and verifies `.replworks/docs/IDEAS.md` and `.replworks/docs/PITCHING_SCRIPT.md` are created empty.
- Init creates `.replworks/docs/` when absent and reports all created paths.
- Existing files with user content are skipped and remain byte for byte unchanged through init and sync.
- Unit tests for the Init Coordinator's create-if-absent behavior pass.
2 changes: 1 addition & 1 deletion internal/command/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ func (r *runner) newInitCommand() *cobra.Command {
return &cobra.Command{
Use: "init",
Short: "Create AGENTS.md and the REPL Works documents",
Long: "init writes the canonical AGENTS.md and ask-human.sh into the project directory, creates the .replworks directory, and creates the four authoritative documents under it as empty files.",
Long: "init writes the canonical AGENTS.md and ask-human.sh into the project directory, creates the four authoritative documents under .replworks as empty files, and creates IDEAS.md and PITCHING_SCRIPT.md under .replworks/docs as empty human-edited files.",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
dir, err := r.projectDir()
Expand Down
20 changes: 20 additions & 0 deletions internal/command/init_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,18 @@ func TestInitAgainstTheLiveCanonicalAndScriptSources(t *testing.T) {
if string(content) != want {
t.Fatal("AGENTS.md does not hold the live canonical AGENTS.md")
}
for _, path := range []string{".replworks/docs/IDEAS.md", ".replworks/docs/PITCHING_SCRIPT.md"} {
content, err := os.ReadFile(filepath.Join(dir, path))
if err != nil {
t.Fatalf("reading %s: %v", path, err)
}
if len(content) != 0 {
t.Fatalf("%s = %q, want an empty file", path, content)
}
if !strings.Contains(stdout, "created: "+path) {
t.Fatalf("stdout = %q, want creation output for %s", stdout, path)
}
}
script, err := os.ReadFile(filepath.Join(dir, ".replworks/scripts/ask-human.sh"))
if err != nil {
t.Fatalf("reading ask-human.sh: %v", err)
Expand Down Expand Up @@ -242,6 +254,14 @@ func TestInitAgainstTheLiveSourceSkipsEveryExistingItem(t *testing.T) {
t.Fatalf("setup: %v", err)
}
}
if err := os.Mkdir(filepath.Join(dir, ".replworks/docs"), 0o755); err != nil {
t.Fatalf("setup: %v", err)
}
for _, document := range []string{"IDEAS.md", "PITCHING_SCRIPT.md"} {
if err := os.WriteFile(filepath.Join(dir, ".replworks/docs", document), []byte("human content: "+document+"\n"), 0o644); err != nil {
t.Fatalf("setup: %v", err)
}
}
if err := os.WriteFile(filepath.Join(dir, "AGENTS.md"), []byte("mine\n"), 0o644); err != nil {
t.Fatalf("setup: %v", err)
}
Expand Down
40 changes: 38 additions & 2 deletions internal/command/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -287,8 +287,8 @@ func TestInitCreatesAgentsAndReplworks(t *testing.T) {
if err != nil {
t.Fatalf("reading .replworks: %v", err)
}
if len(inside) != 5 {
t.Fatalf(".replworks holds %d entries, want 5 including scripts", len(inside))
if len(inside) != 6 {
t.Fatalf(".replworks holds %d entries, want 6 including docs and scripts", len(inside))
}

if got := transport.requested(); len(got) != 2 {
Expand Down Expand Up @@ -625,3 +625,39 @@ func TestInitReplacesExistingAskHumanScriptRegardlessOfInput(t *testing.T) {
t.Fatalf("stdout = %q, want replacement output", stdout)
}
}

func TestInitAndSyncPreserveHumanEditedDocuments(t *testing.T) {
dir := t.TempDir()
if err := os.MkdirAll(filepath.Join(dir, ".replworks/docs"), 0o755); err != nil {
t.Fatalf("setup: %v", err)
}
if err := os.WriteFile(filepath.Join(dir, "AGENTS.md"), []byte(recordedText(t)), 0o644); err != nil {
t.Fatalf("setup: %v", err)
}
wantIdeas := "ideas maintained by a person\n"
wantPitch := "pitch script maintained by a person\n"
if err := os.WriteFile(filepath.Join(dir, ".replworks/docs/IDEAS.md"), []byte(wantIdeas), 0o644); err != nil {
t.Fatalf("setup: %v", err)
}
if err := os.WriteFile(filepath.Join(dir, ".replworks/docs/PITCHING_SCRIPT.md"), []byte(wantPitch), 0o644); err != nil {
t.Fatalf("setup: %v", err)
}
t.Chdir(dir)
client, _ := servePage(t, recordedText(t))

for _, command := range []string{"init", "sync"} {
code, _, stderr := runCommand(t, client, command)
if code != 0 || stderr != "" {
t.Fatalf("%s: code = %d, stderr = %q, want success", command, code, stderr)
}
}
for path, want := range map[string]string{
".replworks/docs/IDEAS.md": wantIdeas,
".replworks/docs/PITCHING_SCRIPT.md": wantPitch,
} {
content, err := os.ReadFile(filepath.Join(dir, path))
if err != nil || string(content) != want {
t.Fatalf("%s = %q, err = %v, want preserved content %q", path, content, err, want)
}
}
}
8 changes: 7 additions & 1 deletion internal/initproject/initproject.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ const (
techStackPath = ".replworks/TECH_STACK.md"
architecturePath = ".replworks/ARCHITECTURE.md"
tasksPath = ".replworks/TASKS.md"
docsPath = ".replworks/docs"
ideasPath = ".replworks/docs/IDEAS.md"
pitchingPath = ".replworks/docs/PITCHING_SCRIPT.md"
scriptsPath = ".replworks/scripts"
askHumanPath = ".replworks/scripts/ask-human.sh"
)
Expand All @@ -26,6 +29,9 @@ var itemPaths = []string{
techStackPath,
architecturePath,
tasksPath,
docsPath,
ideasPath,
pitchingPath,
}

// CanonicalSource provides the canonical AGENTS.md.
Expand Down Expand Up @@ -97,7 +103,7 @@ func (c Coordinator) Run() Outcome {

var err error
switch path {
case replworksPath:
case replworksPath, docsPath:
err = c.Workspace.CreateDirectory(path)
case agentsPath:
err = c.Workspace.CreateFile(path, canonical.Value())
Expand Down
36 changes: 35 additions & 1 deletion internal/initproject/initproject_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,9 @@ func TestRunCreatesEveryEntry(t *testing.T) {
{Path: ".replworks/TECH_STACK.md", Created: true},
{Path: ".replworks/ARCHITECTURE.md", Created: true},
{Path: ".replworks/TASKS.md", Created: true},
{Path: ".replworks/docs", Created: true},
{Path: ".replworks/docs/IDEAS.md", Created: true},
{Path: ".replworks/docs/PITCHING_SCRIPT.md", Created: true},
}
if len(outcome.Items) != len(want) {
t.Fatalf("items = %+v, want %+v", outcome.Items, want)
Expand All @@ -136,6 +139,8 @@ func TestRunCreatesEveryEntry(t *testing.T) {
".replworks/TECH_STACK.md",
".replworks/ARCHITECTURE.md",
".replworks/TASKS.md",
".replworks/docs/IDEAS.md",
".replworks/docs/PITCHING_SCRIPT.md",
} {
content, ok := writer.files[path]
if !ok {
Expand All @@ -148,14 +153,20 @@ func TestRunCreatesEveryEntry(t *testing.T) {
if !writer.dirs[".replworks"] {
t.Fatal(".replworks was not created")
}
if !writer.dirs[".replworks/docs"] {
t.Fatal(".replworks/docs was not created")
}
if source.calls != 1 {
t.Fatalf("canonical source called %d times, want 1", source.calls)
}
wantTrace := "exists AGENTS.md, create-file AGENTS.md, exists .replworks, create-directory .replworks, " +
"exists .replworks/PRODUCT_SPEC.md, create-file .replworks/PRODUCT_SPEC.md, " +
"exists .replworks/TECH_STACK.md, create-file .replworks/TECH_STACK.md, " +
"exists .replworks/ARCHITECTURE.md, create-file .replworks/ARCHITECTURE.md, " +
"exists .replworks/TASKS.md, create-file .replworks/TASKS.md"
"exists .replworks/TASKS.md, create-file .replworks/TASKS.md, " +
"exists .replworks/docs, create-directory .replworks/docs, " +
"exists .replworks/docs/IDEAS.md, create-file .replworks/docs/IDEAS.md, " +
"exists .replworks/docs/PITCHING_SCRIPT.md, create-file .replworks/docs/PITCHING_SCRIPT.md"
if got := writer.trace(); got != wantTrace {
t.Fatalf("trace = %q, want %q", got, wantTrace)
}
Expand Down Expand Up @@ -187,6 +198,26 @@ func TestRunSkipsExistingEntries(t *testing.T) {
}
}

func TestRunPreservesExistingHumanEditedDocuments(t *testing.T) {
writer := newFakeWriter(itemPaths...)
writer.files[ideasPath] = "keep ideas\n"
writer.files[pitchingPath] = "keep pitch\n"

outcome := Coordinator{Source: newSource(), Workspace: writer}.Run()

if len(outcome.Errors) > 0 {
t.Fatalf("errors = %v, want none", outcome.Errors)
}
if writer.files[ideasPath] != "keep ideas\n" || writer.files[pitchingPath] != "keep pitch\n" {
t.Fatalf("human-edited documents changed: %q, %q", writer.files[ideasPath], writer.files[pitchingPath])
}
for _, item := range outcome.Items {
if (item.Path == ideasPath || item.Path == pitchingPath) && (item.Created || item.Replaced || item.Deleted) {
t.Fatalf("item = %+v, want existing file skipped", item)
}
}
}

func TestRunCreatesTheMissingEntryOnly(t *testing.T) {
writer := newFakeWriter("AGENTS.md")

Expand Down Expand Up @@ -216,6 +247,9 @@ func TestRunCreatesOnlyTheMissingDocuments(t *testing.T) {
".replworks/PRODUCT_SPEC.md",
".replworks/ARCHITECTURE.md",
".replworks/TASKS.md",
".replworks/docs",
".replworks/docs/IDEAS.md",
".replworks/docs/PITCHING_SCRIPT.md",
)

outcome := Coordinator{Source: newSource(), Workspace: writer}.Run()
Expand Down
Loading