Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
0877c41
Daglang CLI hardening: DL1–DL4
claude Feb 20, 2026
f07d431
WF14/WF15: universal compilation and codegen capabilities
claude Feb 20, 2026
69606fb
feat: implement Lane 1 Review/SDLC core (W1-W7)
claude Feb 20, 2026
b7c7611
[WF6][WF7][WF8][WF9] add workflow executor, Makefile cutover, and SLO…
claude Feb 20, 2026
e2aff9d
Consolidate workflow CLI dispatch into single-source-of-truth registry
claude Feb 20, 2026
34dfc8a
Implement Lane 5 remaining tool capabilities (WF19-WF22)
claude Feb 20, 2026
87e85d0
Return Result from build_context instead of calling process::exit
claude Feb 20, 2026
398e770
fix: wire requirements context into pipeline criteria and populate Pr…
claude Feb 20, 2026
7765a49
Merge remote-tracking branch 'origin/claude/review-sdlc-core-LFc5V' i…
cursoragent Feb 20, 2026
de9eec5
Merge remote-tracking branch 'origin/claude/workflow-cutover-lane-2-t…
cursoragent Feb 20, 2026
d9badf3
Merge remote-tracking branch 'origin/claude/remaining-tool-capabiliti…
cursoragent Feb 20, 2026
dff5c8e
Merge remote-tracking branch 'origin/claude/universal-capabilities-wf…
cursoragent Feb 20, 2026
6c20445
Merge remote-tracking branch 'origin/claude/daglang-hardening-Sp5i3' …
cursoragent Feb 20, 2026
0c0ab9c
Replace unstable is_multiple_of calls with modulo checks
cursoragent Feb 20, 2026
a6175bd
Align daglang tests and runtime derive with hardening behavior
cursoragent Feb 20, 2026
de136c4
Enforce .dag directory conflict handling for modules command
cursoragent Feb 20, 2026
90189c1
Use canonical .dag directory conflict wording across commands
cursoragent Feb 20, 2026
3c31d25
Refresh daglang workflow fixture snapshots for merged behavior
cursoragent Feb 20, 2026
b298114
Regenerate daglang workflow contract fixtures
cursoragent Feb 20, 2026
c8b153a
Normalize daglang workflow fixture paths and contract statuses
cursoragent Feb 20, 2026
d88c128
Block unsupported workflow execution mode in gunbc-workflow
cursoragent Feb 20, 2026
9c32887
Recover WF16-WF18 gist workflow mode planning
cursoragent Feb 20, 2026
abedfe8
Fix workflow execution migration and remove legacy fallbacks
cursoragent Feb 20, 2026
9629b27
Fix clippy single-element-loop in build-all registry
cursoragent Feb 20, 2026
ca5baf7
Wire W4 dimension review graph and preserve aspirational severity
cursoragent Feb 20, 2026
b9bb1da
Fix W4 dimension graph node ordering cycles
cursoragent Feb 20, 2026
a36fea9
Harden workflow dispatch and fail loud on unknown run args
cursoragent Feb 20, 2026
fa7ce26
Fix clippy manual-is-multiple-of in simulator
cursoragent Feb 20, 2026
a62a08f
Use bitwise even check for simulator bool seed
cursoragent Feb 20, 2026
5d259a3
Allow disallowed filesystem methods in executor contract tests
cursoragent Feb 20, 2026
45f4919
Allow filesystem cleanup calls in tool capability tests
cursoragent Feb 20, 2026
101b9b0
Fix clippy expect-fun-call in workflow parser test
cursoragent Feb 20, 2026
cdc9421
Fix codegen verify/fix unit command arguments
cursoragent Feb 20, 2026
d82e1f1
Use codegen DAG binary for verify/fix workflow units
cursoragent Feb 20, 2026
7be29df
Update daglang resolve module graph snapshots for github issues module
cursoragent Feb 20, 2026
7f9d295
Refresh daglang syntax corpus inventory for github issues module
cursoragent Feb 20, 2026
8c5bd47
Update daglang syntax golden file counts to 46
cursoragent Feb 20, 2026
34f659b
Update std/types representative AST item count
cursoragent Feb 20, 2026
bda1eda
Refresh std/types representative type signatures
cursoragent Feb 20, 2026
a1293f6
Restore tool passthrough args and order-independent plan format parsing
cursoragent Feb 20, 2026
c95bb8c
Tighten workflow wrapper boundaries and cover monolithic terminal units
cursoragent Feb 20, 2026
56651fb
Remove makefile workflow wrapper entrypoint arg expansion
cursoragent Feb 20, 2026
5d2c1fa
Add regression coverage for terminal branch command mappings
cursoragent Feb 20, 2026
6b267eb
Fix base64 length check clippy lint in gcp ops
cursoragent Feb 21, 2026
00e9aec
Fix resolve service base64 length clippy lint
cursoragent Feb 21, 2026
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
12 changes: 6 additions & 6 deletions TODO/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,12 @@ Use these lanes to assign workers with minimal overlap and clear stop conditions
|---|---|---|---|---|---|---|
| A: Resolver de-stringing | `P12` -> `P6` | none | `resolve.rs`, `daglang-lower`, runtime resolver/dispatch | no string-prefix op resolution; no deferred passthrough fallback | `cargo test --workspace`, resolver golden tests | **DONE** |
| B: Workflow planner core | `WF1` -> `WF2` -> `WF3` -> `WF4` -> `WF5` | `WF1-D`..`WF4-D` reviewed | `gunbc-dag` workflow schema/planner/ledger/executor | deterministic typed plan, claim-safe admission, key/rehydration correctness | `cargo test --workspace` | **DONE** |
| C: Workflow cutover/perf | `WF6` -> `WF7` -> `WF8` -> `WF9` | Lane B complete ✓ | workflow entrypoints + `Makefile` wrappers + CI wiring | `make ci`/`make test-all` use planner path with SLO telemetry | `make ci`, `make test-all`, CI dry run | **OPEN (unblocked)** |
| C: Workflow cutover/perf | `WF6` -> `WF7` -> `WF8` -> `WF9` | Lane B complete ✓ | workflow entrypoints + `Makefile` wrappers + CI wiring | `make ci`/`make test-all` use planner path with SLO telemetry | `make ci`, `make test-all`, CI dry run | **DONE** |
| D: Modeling hardening graph/runtime | `M8` -> `M9` -> `M16` and `M10` -> `M11` | `-D` tasks approved | IR type DAG/system-model/transport + runtime resource/dry-run | metadata inertness, typed dependency markers, strict dry-run enforced | targeted model tests + `cargo test --workspace` | **DONE** |
| E: Security/install/process drift | `M7`, `M15`, `M17` -> `M18` -> `M19` | `-D` tasks approved | value redaction, installer model, proof harness | no accidental secret leak path; typed PM policy; invariants testable | test suites for each module + planner invariant suite | **DONE** |
| F: Universal capabilities | `WF14-D` -> `WF14` -> `WF15-D` -> `WF15` | Lane B complete ✓ | binary dispatch, codegen keyed unit, planner integration | compilation + codegen capabilities keyed and shared across all workflows | `gunbc-workflow --plan gist-snapshot` shows codegen CachedHit | **OPEN (design done, impl unblocked)** |
| G: Gist capability stack | `WF16-D` -> `WF16` -> (`WF17`, `WF18`) | Lane F complete | gist graph, gist_modes, credential chain, git state units | base gist workflow built; diff + recent augment base; all modes use planner path | `make gist` warm path, credential sharing across gist/dag-viz | **OPEN (design done, blocked on F)** |
| H: Remaining capabilities | `WF19-D` -> `WF19` -> `WF20` -> `WF21` -> `WF22` | Lane F complete | bootstrap/makegen/pragma/deps/dag-viz, Makefile | FS write + generator capabilities minimized; all tools on planner path with verification | per-capability hit/miss reporting, cross-workflow sharing observable | **OPEN (design done, blocked on F)** |
| H: Remaining capabilities | `WF19-D` -> `WF19` -> `WF20` -> `WF21` -> `WF22` | Lane F complete | bootstrap/makegen/pragma/deps/dag-viz, Makefile | FS write + generator capabilities minimized; all tools on planner path with verification | per-capability hit/miss reporting, cross-workflow sharing observable | **DONE** |
| I: Service codegen | `SC1` -> `SC2` -> `SC3` -> (`SC4`, `SC5`) -> `SC6` -> `SC7` | none | daglang-lower, resolve.rs, daglang-emit/*, service .dag files | 3 protocol interfaces replace all per-service Rust; all emission targets generate service code from DSL | `make gist --dry-run` uses generic interpreter | **DONE** |
| J: SDLC pipeline | `W9` -> `W10` -> `W11` -> `W12` -> `W13` | W1-W3 (credentials) | `dsl/pipelines/sdlc.dag`, `lib/ticket-ops/`, `lib/design-ops/` | issue-centric pipeline: post issue → design → review → implement → close | `gunbc sdlc --issue 42` runs full lifecycle | **OPEN (design done, blocked on W1-W3)** |

Expand Down Expand Up @@ -517,15 +517,15 @@ modes compose on top. See design doc Section 15.4.
| ID | Task | Deps | Size |
|----|------|------|------|
| **WF19-D** | **[DONE 2026-02-20]** **Generator + remaining tool capability design spec**: bootstrap/makegen/pragma/deps/dag-viz decomposed. **Design doc**: `docs/design/workflow/tool-workflow-design-pack.md` (Sections 5-9). | WF1-D, WF3-D, WF15-D | M |
| **WF19** | **Generator workflow capability port (bootstrap/makegen/pragma)**: implement planner path with keyed generation + filesystem-upsert capabilities. **Acceptance**: warm no-op executes zero capability units; generation step skips when generation inputs (registry data, templates, config) are unchanged; filesystem write skips as consequence. | WF19-D, WF5, WF14, WF15 | M |
| **WF20** | **Remaining tool capability port (deps/dag-viz/dag-snapshot)**: implement planner path for remaining tools. dag-viz modes reuse git state + credential capability units from gist family. **Acceptance**: warm no-op executes zero capability units; dag-viz credential resolution shared with gist via global ledger. | WF19-D, WF5, WF14, WF15 | M |
| **WF19** | **[DONE 2026-02-20]** **Generator workflow capability port (bootstrap/makegen/pragma)**: planner-managed workflow specs with keyed compilation_ensure + codegen_ensure capabilities. Bootstrap (8 nodes, 2 parallel upsert chains), makegen (6 nodes, linear chain), pragma (9 nodes, 3 parallel render+upsert chains). Universal capabilities share canonical WorkIdentity across all workflows via global dedup. | WF19-D, WF5, WF14, WF15 | M |
| **WF20** | **[DONE 2026-02-20]** **Remaining tool capability port (deps/dag-viz/dag-snapshot)**: planner-managed workflow specs for deps (10 nodes, parallel install+generate), dag-viz (8 nodes, shared branch_resolution + credential_resolve), dag-snapshot (9 nodes, mirrors gist-snapshot). dag-viz/dag-snapshot share base capabilities via canonical identity dedup. | WF19-D, WF5, WF14, WF15 | M |

### Phase T-D: Cutover + Verification

| ID | Task | Deps | Size |
|----|------|------|------|
| **WF21** | **Makefile thinning for all tool targets**: convert all `make <tool>` targets to thin wrappers over `gunbc-workflow <tool>`; remove `ensure-codegen` as prerequisite; remove `cargo run` invocations for planner-managed tools. **Acceptance**: all tool Make targets are transport-only shims; no duplicate orchestration remains. | WF16, WF17, WF18, WF19, WF20 | S |
| **WF22** | **Capability minimization verification**: extend WF9 instrumentation to all tool targets. Planner reports per-capability hit/miss/execute status. **Acceptance**: `gunbc-workflow --plan gist-snapshot` (and all tools) emits capability-level breakdown (which capabilities hit, which missed, why); cross-workflow sharing is observable (e.g., "credential.resolve: CachedHit from gist-diff run"). | WF9, WF21 | S |
| **WF21** | **[DONE 2026-02-20]** **Makefile thinning for all tool targets**: `ToolInfo.planner_managed` flag added; planner-managed tools (bootstrap, makegen, pragma, deps, dag-viz*, dag-snapshot) dispatch via `target/release/gunbc-workflow --plan <tool>`; `ensure-codegen` prerequisite removed for these targets. Non-planner tools (gist, ci, build-all) retain `cargo run` dispatch. | WF16, WF17, WF18, WF19, WF20 | S |
| **WF22** | **[DONE 2026-02-20]** **Capability minimization verification**: `PlanExplain` extended with `capability_status: BTreeMap<String, CapabilityStatus>` providing per-capability hit/miss/execute breakdown. `CapabilityAction` enum (CachedHit/Execute) with node_id tracking. CLI text output includes `capabilities:` section; JSON output includes `capabilities` array with action/detail/node_ids. Cross-workflow sharing observable via canonical capability names. | WF9, WF21 | S |

### Lane Extension (Updated from Sprint 5)

Expand Down
22 changes: 19 additions & 3 deletions core/daglang/daglang-cli/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ pub(super) fn dispatch(args: &[String], cwd: &std::path::Path) {
eprintln!("{error}");
std::process::exit(1);
}
if let Some(error) = path_utils::check_dag_directory_conflict(&normalized) {
eprintln!("{error}");
std::process::exit(1);
}
}
let roots = if let Some(root) = root_arg {
vec![resolve_root(cwd, Some(&root))]
Expand Down Expand Up @@ -132,11 +136,17 @@ pub(super) fn dispatch(args: &[String], cwd: &std::path::Path) {
} else {
None
};
let context = build_check_pipeline_context_with_default_roots(
let context = match build_check_pipeline_context_with_default_roots(
cwd,
args.get(2),
configured_default_roots.as_deref(),
);
) {
Ok(context) => context,
Err(error) => {
eprintln!("{error}");
std::process::exit(1);
}
};
let result = run_pipeline_or_exit(&context, PipelineStop::Build);
if !result.diagnostics().is_empty() {
for diagnostic in result.diagnostics() {
Expand Down Expand Up @@ -246,7 +256,13 @@ pub(super) fn dispatch(args: &[String], cwd: &std::path::Path) {
ExecutionMode::DryRun(makegen_check_mode_transport_mocks(&output_path_str))
}
};
let context = build_context(cwd, Some(&parsed.input_path));
let context = match build_context(cwd, Some(&parsed.input_path)) {
Ok(context) => context,
Err(error) => {
eprintln!("{error}");
std::process::exit(1);
}
};
let log = match compile_resolve_execute_from_context(&context, mode, Some(&input_mocks))
{
Ok(log) => log,
Expand Down
20 changes: 12 additions & 8 deletions core/daglang/daglang-cli/src/compile/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,27 @@ use super::{resolve_lowered_dag, CheckOutput, CompileError, CompileOptions, Comp

/// Builds compile pipeline context from CLI input.
///
/// Compatibility note: paths ending in `.dag` are always treated as
/// single-file targets, even when they point to a directory.
/// This only applies to the strict lowercase `.dag` extension.
/// Wrong-cased dag-like extensions (`.DAG`, `.DaG`, etc.) are handled by
/// higher-level CLI validation and are not treated as single-file targets.
pub fn build_context(cwd: &std::path::Path, input: Option<&String>) -> PipelineContext {
/// Paths ending in `.dag` that are regular files are treated as single-file
/// targets. Directories named with a `.dag` suffix are rejected with an
/// explicit error — callers should pass the directory path without the
/// `.dag` suffix or reference a `.dag` file inside it.
pub fn build_context(cwd: &std::path::Path, input: Option<&String>) -> Result<PipelineContext, String> {
let parsed = input.map(|value| path_utils::normalize_cli_path(cwd, &PathBuf::from(value)));
if let Some(ref path) = parsed {
if let Some(error) = path_utils::check_dag_directory_conflict(path) {
return Err(error);
}
}
let (roots, target_file) = match parsed {
Some(path) if path_utils::is_single_file_target(&path, true) => {
Some(path) if path_utils::is_single_file_target(&path) => {
let root = path_utils::resolve_single_file_root(cwd, &path);
(vec![root], Some(path))
}
Some(path) => (vec![path], None),
None => (vec![path_utils::resolve_default_root(cwd)], None),
};

PipelineContext { roots, target_file }
Ok(PipelineContext { roots, target_file })
}

pub fn compile_from_context(context: &PipelineContext) -> Result<CompileOutput, CompileError> {
Expand Down
58 changes: 32 additions & 26 deletions core/daglang/daglang-cli/src/compile/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ fn build_context_normalizes_absolute_directory_input_components() {
let input_str = input.to_string_lossy().to_string();

let cwd = std::env::temp_dir();
let context = build_context(&cwd, Some(&input_str));
let context = build_context(&cwd, Some(&input_str)).expect("build_context should succeed");
assert_eq!(context.roots, vec![normalized_root.clone()]);
assert!(context.target_file.is_none());

Expand Down Expand Up @@ -155,7 +155,7 @@ fn build_context_normalizes_absolute_single_file_input_components() {
let input_str = input.to_string_lossy().to_string();

let cwd = std::env::temp_dir();
let context = build_context(&cwd, Some(&input_str));
let context = build_context(&cwd, Some(&input_str)).expect("build_context should succeed");
assert_eq!(
context.roots,
vec![normalized_file
Expand All @@ -178,7 +178,7 @@ fn build_context_default_root_is_cwd_dsl() {
.expect("system clock should be after unix epoch")
.as_nanos()
));
let context = build_context(&cwd, None);
let context = build_context(&cwd, None).expect("build_context should succeed");
assert_eq!(context.roots, vec![cwd.join("dsl")]);
assert!(context.target_file.is_none());
}
Expand All @@ -195,7 +195,7 @@ fn run() -> Unit { }
.expect("failed to write check valid fixture");
let cwd = std::env::temp_dir();
let input = fixture.to_string_lossy().to_string();
let context = build_context(&cwd, Some(&input));
let context = build_context(&cwd, Some(&input)).expect("build_context should succeed");

let output = check_from_context(&context).expect("check should succeed");
assert_eq!(
Expand All @@ -218,7 +218,7 @@ fn run() -> String { return 42 }
.expect("failed to write check invalid fixture");
let cwd = std::env::temp_dir();
let input = fixture.to_string_lossy().to_string();
let context = build_context(&cwd, Some(&input));
let context = build_context(&cwd, Some(&input)).expect("build_context should succeed");

let error = check_from_context(&context).expect_err("check should fail");
assert_typecheck_stage_error(&error);
Expand All @@ -231,7 +231,7 @@ fn run() -> String { return 42 }
}

#[test]
fn build_context_treats_dag_directory_input_as_single_file_target() {
fn build_context_rejects_dag_directory_input() {
let root = std::env::temp_dir().join(format!(
"daglang_build_context_dag_dir_target_{}_{}",
std::process::id(),
Expand All @@ -243,24 +243,23 @@ fn build_context_treats_dag_directory_input_as_single_file_target() {
let dag_dir = root.join("bundle.dag");
std::fs::create_dir_all(dag_dir.join("nested"))
.expect("failed to create .dag directory fixture");
std::fs::write(
dag_dir.join("nested/main.dag"),
"module sample.main\nfn run() -> Unit {}",
)
.expect("failed to write nested dag fixture");

let input_str = dag_dir.to_string_lossy().to_string();
let cwd = std::env::temp_dir();
let context = build_context(&cwd, Some(&input_str));

assert_eq!(context.roots, vec![root.clone()]);
assert_eq!(context.target_file, Some(dag_dir.clone()));
let error = crate::path_utils::check_dag_directory_conflict(&dag_dir);
assert!(
error.is_some(),
".dag directory should be rejected with explicit error"
);
assert!(
error.as_ref().unwrap().contains("is a directory"),
"error message should mention directory: {:?}",
error
);

std::fs::remove_dir_all(root).expect("failed to cleanup temp root");
}

#[test]
fn build_context_normalizes_trailing_slash_for_dag_directory_target() {
fn build_context_rejects_dag_directory_with_trailing_slash() {
let root = std::env::temp_dir().join(format!(
"daglang_build_context_dag_dir_trailing_{}_{}",
std::process::id(),
Expand All @@ -274,10 +273,13 @@ fn build_context_normalizes_trailing_slash_for_dag_directory_target() {
let input_with_trailing_slash = format!("{}/", dag_dir.display());

let cwd = std::env::temp_dir();
let context = build_context(&cwd, Some(&input_with_trailing_slash));

assert_eq!(context.roots, vec![root.clone()]);
assert_eq!(context.target_file, Some(dag_dir.clone()));
let normalized =
crate::path_utils::normalize_cli_path(&cwd, &PathBuf::from(&input_with_trailing_slash));
let error = crate::path_utils::check_dag_directory_conflict(&normalized);
assert!(
error.is_some(),
".dag directory with trailing slash should be rejected"
);

std::fs::remove_dir_all(root).expect("failed to cleanup temp root");
}
Expand All @@ -297,7 +299,8 @@ fn build_context_treats_uppercase_dag_directory_with_trailing_slash_as_root() {
let input_with_trailing_slash = format!("{}/", dag_dir.display());

let cwd = std::env::temp_dir();
let context = build_context(&cwd, Some(&input_with_trailing_slash));
let context = build_context(&cwd, Some(&input_with_trailing_slash))
.expect("build_context should succeed for non-lowercase .dag extension");

assert_eq!(context.roots, vec![dag_dir.clone()]);
assert_eq!(context.target_file, None);
Expand All @@ -320,7 +323,8 @@ fn build_context_treats_mixed_case_dag_directory_with_trailing_slash_as_root() {
let input_with_trailing_slash = format!("{}/", dag_dir.display());

let cwd = std::env::temp_dir();
let context = build_context(&cwd, Some(&input_with_trailing_slash));
let context = build_context(&cwd, Some(&input_with_trailing_slash))
.expect("build_context should succeed for non-lowercase .dag extension");

assert_eq!(context.roots, vec![dag_dir.clone()]);
assert_eq!(context.target_file, None);
Expand All @@ -343,7 +347,8 @@ fn build_context_treats_uppercase_dag_directory_input_as_root() {

let input_str = dag_dir.to_string_lossy().to_string();
let cwd = std::env::temp_dir();
let context = build_context(&cwd, Some(&input_str));
let context = build_context(&cwd, Some(&input_str))
.expect("build_context should succeed for non-lowercase .dag extension");

assert_eq!(context.roots, vec![dag_dir.clone()]);
assert_eq!(context.target_file, None);
Expand All @@ -366,7 +371,8 @@ fn build_context_treats_mixed_case_dag_directory_input_as_root() {

let input_str = dag_dir.to_string_lossy().to_string();
let cwd = std::env::temp_dir();
let context = build_context(&cwd, Some(&input_str));
let context = build_context(&cwd, Some(&input_str))
.expect("build_context should succeed for non-lowercase .dag extension");

assert_eq!(context.roots, vec![dag_dir.clone()]);
assert_eq!(context.target_file, None);
Expand Down
Loading